mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
Compare commits
228 Commits
Author | SHA1 | Date | |
---|---|---|---|
4c3ebc055d | |||
61c1958717 | |||
3ab1a23ac5 | |||
a81c780e1e | |||
2baeb2eb5b | |||
e6b011b41b | |||
308467006b | |||
ecf1359336 | |||
a12dc4882e | |||
980d0a5810 | |||
eb3d84ee9d | |||
6b43057333 | |||
071ded0eac | |||
47c4338c9e | |||
dfdeea4b6c | |||
cbb7936149 | |||
c08e1307e8 | |||
25b86a057f | |||
ef72fa405d | |||
1eb773fb57 | |||
439b6d0326 | |||
ef0081f711 | |||
8c7a24b3d7 | |||
c76026c9f1 | |||
37e4ecbf47 | |||
58dc38f72e | |||
36c5472987 | |||
717542e045 | |||
940bd0405f | |||
d40e8879d1 | |||
6a67510bdc | |||
a79521a37d | |||
4667194271 | |||
a20852b03a | |||
ec0f1cc0d3 | |||
49d9650d35 | |||
1b876c45cc | |||
23f52f25e2 | |||
16d7d80183 | |||
5389fe00dd | |||
cd67674976 | |||
36673a49a4 | |||
b84b3ba9f4 | |||
5343d8c934 | |||
80891b0b46 | |||
48313685ec | |||
7dc47c00ac | |||
830b5bb934 | |||
8d43984644 | |||
1b152c1a80 | |||
175b9f0cfb | |||
16180be7e2 | |||
11b270ee68 | |||
97ab8a4475 | |||
da3b864e02 | |||
0bd989e493 | |||
59c4d2022c | |||
1ec17caa1d | |||
252618a975 | |||
315c7660d1 | |||
6a6809b07f | |||
a960b5928e | |||
46d96d7bb4 | |||
69063a9cf7 | |||
7c8d2f54c7 | |||
49aed23f8c | |||
3418f85292 | |||
361afd87aa | |||
7f6815c274 | |||
4ecd70330a | |||
ae13905512 | |||
e3377d0f28 | |||
b15061a110 | |||
6d6945bf44 | |||
cf7c526d4f | |||
8e21be0340 | |||
37265ccb4f | |||
169fbe3b93 | |||
617b378c36 | |||
438e954dd6 | |||
df1a0e20ee | |||
7b3c286790 | |||
fb7d7c75cf | |||
1eafe58d16 | |||
e7a909dc4e | |||
9deee9ddba | |||
e769c2103f | |||
ca9cdf0379 | |||
7ef471381c | |||
48e9b20836 | |||
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 | |||
83720999d8 | |||
79bbf3d71e | |||
1dec69d726 | |||
ef2db68430 | |||
e6f3250286 | |||
ca56f1dd07 | |||
6bd973a803 | |||
49dd703e12 | |||
0f27441225 | |||
97d76ddca8 | |||
891d491cdb | |||
2a1fb0eddf | |||
5c25fbe2c4 | |||
e3e618d3cf | |||
e2489a2487 | |||
bb17027cc9 | |||
6b17d3eb7d | |||
c5f11eff8b | |||
b36bc5e47d |
@ -1,14 +1,9 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
.github
|
||||
.git
|
||||
.gitignore
|
||||
.editorconfig
|
||||
.idea
|
||||
.vscode
|
||||
test
|
||||
temp
|
||||
tmp
|
||||
LICENSE
|
||||
Makefile
|
||||
error-pages
|
||||
## Ignore everything
|
||||
*
|
||||
|
||||
## Except the following files and directories
|
||||
!/cmd
|
||||
!/internal
|
||||
!/templates
|
||||
!/error-pages.yml
|
||||
!/go.*
|
||||
|
@ -7,11 +7,8 @@ charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml, yaml, sh, conf}]
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[{Makefile, go.mod, *.go}]
|
||||
indent_style = tab
|
||||
|
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
|
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates>
|
||||
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule: {interval: monthly}
|
||||
reviewers: [tarampampam]
|
||||
assignees: [tarampampam]
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule: {interval: monthly}
|
||||
reviewers: [tarampampam]
|
||||
assignees: [tarampampam]
|
||||
|
||||
- package-ecosystem: docker
|
||||
directory: /
|
||||
schedule: {interval: monthly}
|
||||
reviewers: [tarampampam]
|
||||
assignees: [tarampampam]
|
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"
|
||||
]
|
||||
}
|
24
.github/workflows/dependabot.yml
vendored
Normal file
24
.github/workflows/dependabot.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: dependabot
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot: # https://tinyurl.com/e69djmen
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- uses: dependabot/fetch-metadata@v1
|
||||
id: metadata
|
||||
with: {github-token: "${{ secrets.GITHUB_TOKEN }}"}
|
||||
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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-latest
|
||||
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
|
55
.github/workflows/release.yml
vendored
55
.github/workflows/release.yml
vendored
@ -5,33 +5,44 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
purge-cdn-cache:
|
||||
name: Purge jsDelivr CDN cache
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [linux, darwin] # linux, freebsd, darwin, windows
|
||||
arch: [amd64] # amd64, 386
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with: {go-version: 1.17.1}
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gacts/setup-go-with-cache@v1
|
||||
with: {go-version-file: go.mod}
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
- {uses: gacts/github-slug@v1, id: slug}
|
||||
|
||||
- name: Generate builder values
|
||||
id: values
|
||||
run: echo "::set-output name=binary-name::error-pages-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
run: echo "binary-name=error-pages-${{ matrix.os }}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
CGO_ENABLED: 0
|
||||
LDFLAGS: -s -w -X github.com/tarampampam/error-pages/internal/version.version=${{ steps.slug.outputs.version }}
|
||||
LDFLAGS: -s -w -X gh.tarampamp.am/error-pages/internal/version.version=${{ steps.slug.outputs.version }}
|
||||
run: go build -trimpath -ldflags "$LDFLAGS" -o "./${{ steps.values.outputs.binary-name }}" ./cmd/error-pages/
|
||||
|
||||
- name: Upload binary file to release
|
||||
@ -44,29 +55,28 @@ jobs:
|
||||
|
||||
docker-image:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
- {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@v4 # Action page: <https://github.com/docker/build-push-action>
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
@ -75,17 +85,20 @@ 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:
|
||||
name: Update the demonstration
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker-image]
|
||||
steps:
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
- {uses: gacts/github-slug@v1, id: slug}
|
||||
|
||||
- name: Take rendered templates from the built docker image
|
||||
run: |
|
||||
|
169
.github/workflows/tests.yml
vendored
169
.github/workflows/tests.yml
vendored
@ -8,53 +8,78 @@ 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
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: gacts/setup-go-with-cache@v1
|
||||
with: {go-version: 1.19}
|
||||
|
||||
- uses: golangci/golangci-lint-action@v3
|
||||
with: {skip-pkg-cache: true, skip-build-cache: true}
|
||||
|
||||
validate-config-file:
|
||||
name: Validate config file
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: gacts/setup-node-with-cache@v1
|
||||
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-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: gacts/setup-node-with-cache@v1
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with: {go-version: 1.17}
|
||||
|
||||
- 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
|
||||
id: go-cache
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
|
||||
- if: steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: go mod download
|
||||
- uses: gacts/setup-go-with-cache@v1
|
||||
with: {go-version-file: go.mod}
|
||||
|
||||
- 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
|
||||
@ -62,44 +87,34 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
|
||||
build:
|
||||
name: Build for ${{ matrix.os }} (${{ matrix.arch }})
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
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/checkout@v3
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gacts/setup-go-with-cache@v1
|
||||
with: {go-version-file: go.mod}
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
|
||||
- run: go mod download
|
||||
- {uses: gacts/github-slug@v1, id: slug}
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
CGO_ENABLED: 0
|
||||
LDFLAGS: -s -w -X github.com/tarampampam/error-pages/internal/version.version=${{ steps.slug.outputs.branch-name-slug }}@${{ steps.slug.outputs.commit-hash-short }}
|
||||
LDFLAGS: -s -w -X gh.tarampamp.am/error-pages/internal/version.version=${{ steps.slug.outputs.branch-name-slug }}@${{ steps.slug.outputs.commit-hash-short }}
|
||||
run: go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/
|
||||
|
||||
- name: Try to execute
|
||||
if: matrix.os == 'linux'
|
||||
run: ./error-pages version && ./error-pages -h
|
||||
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
|
||||
@ -108,12 +123,12 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
|
||||
generate:
|
||||
name: Run templates generator
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
@ -123,7 +138,7 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
run: mv ./error-pages ./../error-pages && chmod +x ./../error-pages
|
||||
|
||||
- name: Run generator
|
||||
run: ./error-pages build ./out --verbose --index
|
||||
run: ./error-pages --verbose build --index ./out
|
||||
|
||||
- name: Test files creation
|
||||
run: |
|
||||
@ -134,18 +149,23 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
test -f ./out/shuffle/404.html
|
||||
test -f ./out/noise/404.html
|
||||
test -f ./out/hacker-terminal/404.html
|
||||
test -f ./out/cats/404.html
|
||||
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
|
||||
test -f ./out/orient/404.html
|
||||
|
||||
docker-image:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [golangci-lint, go-test]
|
||||
runs-on: ubuntu-latest
|
||||
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: 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@v4 # Action page: <https://github.com/docker/build-push-action>
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
@ -155,7 +175,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
|
||||
@ -163,30 +183,38 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
|
||||
scan-docker-image:
|
||||
name: Scan the docker image
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
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.11.2 # 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
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
@ -194,16 +222,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" --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: curl --fail http://127.0.0.1:8080/500.html
|
||||
- run: curl --fail http://127.0.0.1:8080/400.html
|
||||
- run: curl --fail http://127.0.0.1:8080/health/live
|
||||
- run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/) -eq 404
|
||||
- run: hurl --color --test --fail-at-end --variable host=127.0.0.1 --variable port=8080 ./test/hurl/*.hurl
|
||||
|
||||
- name: Stop the container
|
||||
if: always()
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,7 +3,7 @@
|
||||
/.vscode
|
||||
|
||||
## Binaries
|
||||
error-pages
|
||||
/error-pages
|
||||
|
||||
## Temp dirs & trash
|
||||
/temp
|
||||
|
@ -5,6 +5,8 @@ run:
|
||||
skip-dirs:
|
||||
- .github
|
||||
- .git
|
||||
- tmp
|
||||
- temp
|
||||
modules-download-mode: readonly
|
||||
allow-parallel-runners: true
|
||||
|
||||
@ -40,9 +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
|
||||
- deadcode # Finds unused code
|
||||
- depguard # Go linter that checks if package imports are in a list of acceptable packages
|
||||
- bidichk # Checks for dangerous unicode character sequences
|
||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
|
||||
- dupl # Tool for code clone detection
|
||||
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
|
||||
@ -69,19 +69,13 @@ 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
|
||||
- wsl # Whitespace Linter - Forces you to use empty lines!
|
||||
|
||||
@ -93,4 +87,3 @@ issues:
|
||||
- funlen
|
||||
- scopelint
|
||||
- gocognit
|
||||
- noctx
|
||||
|
299
CHANGELOG.md
299
CHANGELOG.md
@ -4,6 +4,305 @@ 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.25.0
|
||||
|
||||
### Added
|
||||
|
||||
- Go updated from `1.20` up to `1.21`
|
||||
- Error pages now translated into 🇮🇩 [#218]
|
||||
- Possibility catch all paths with error page 404 (using `--catch-all` flag for the `serve` or environment variable `CATCH_ALL=true`) [#217]
|
||||
|
||||
[#218]:https://github.com/tarampampam/error-pages/pull/218
|
||||
[#217]:https://github.com/tarampampam/error-pages/issues/217
|
||||
|
||||
## v2.24.0
|
||||
|
||||
### Added
|
||||
|
||||
- Support for IPv6 addresses in the `--listen` flag [#191]
|
||||
|
||||
[#191]:https://github.com/tarampampam/error-pages/issues/191
|
||||
|
||||
## v2.23.0
|
||||
|
||||
### Added
|
||||
|
||||
- Template `orient` [#190]
|
||||
|
||||
[#190]:https://github.com/tarampampam/error-pages/pull/190
|
||||
|
||||
## v2.22.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Non-existing pages now return styled `404` status page (with `404` status code) [#188]
|
||||
|
||||
[#188]:https://github.com/tarampampam/error-pages/issues/188
|
||||
|
||||
## v2.21.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Go updated from `1.19` up to `1.20`
|
||||
- Go dependencies updated
|
||||
- Module name changed from `github.com/tarampampam/error-pages` to `gh.tarampamp.am/error-pages`
|
||||
|
||||
## v2.20.0
|
||||
|
||||
### Changed
|
||||
|
||||
- `version` subcommand replaced by `--version` flag [#163]
|
||||
- `--config-file` flag is not global anymore (use `error-pages (serve|build) --config-file ...` instead of `error-pages --config-file ... (serve|build) ...`) [#163]
|
||||
- Flags `--verbose`, `--debug` and `--log-json` are deprecated, use `--log-level` and `--log-format` instead [#163]
|
||||
|
||||
### Added
|
||||
|
||||
- Possibility to use custom env variables in templates [#164], [#165]
|
||||
|
||||
[#164]:https://github.com/tarampampam/error-pages/issues/164
|
||||
[#165]:https://github.com/tarampampam/error-pages/pull/165
|
||||
[#163]:https://github.com/tarampampam/error-pages/pull/163
|
||||
|
||||
## v2.19.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Go updated from `1.18` up to `1.19`
|
||||
|
||||
### Added
|
||||
|
||||
- Error pages now translated into Chinese 🇨🇳 [#147]
|
||||
|
||||
[#147]:https://github.com/tarampampam/error-pages/pull/147
|
||||
|
||||
## v2.18.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced `fonts.googleapis.com` by `fonts.bunny.net` regarding GDPR compliance [#131]
|
||||
|
||||
[#131]:https://github.com/tarampampam/error-pages/pull/131
|
||||
|
||||
## 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
|
||||
|
||||
- Flag `--default-http-code` for the `serve` subcommand (`404` is used by default instead of `200`, environment name `DEFAULT_HTTP_CODE`) [#41]
|
||||
|
||||
### Changed
|
||||
|
||||
- Go updated from `1.17.1` up to `1.17.5`
|
||||
|
||||
[#41]:https://github.com/tarampampam/error-pages/issues/41
|
||||
|
||||
## v2.2.0
|
||||
|
||||
### Added
|
||||
|
||||
- Template `cats` [#31]
|
||||
|
||||
[#31]:https://github.com/tarampampam/error-pages/pull/31
|
||||
|
||||
## v2.1.0
|
||||
|
||||
### Added
|
||||
|
||||
- `referer` field in access log records
|
||||
- Flag `--default-error-page` for the `serve` subcommand (`404` is used by default, environment name `DEFAULT_ERROR_PAGE`)
|
||||
|
||||
### Changed
|
||||
|
||||
- The source code has been refactored
|
||||
- The index page (`/`) now returns the error page with a code, declared using `--default-error-page` flag (HTTP code 200, when a page code exists)
|
||||
|
||||
## v2.0.0
|
||||
|
||||
### Changed
|
||||
|
23
Dockerfile
23
Dockerfile
@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1.2
|
||||
|
||||
# Image page: <https://hub.docker.com/_/golang>
|
||||
FROM golang:1.17.1-alpine as builder
|
||||
FROM golang:1.21-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"
|
||||
@ -11,12 +11,12 @@ WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
# arguments to pass on each go tool link invocation
|
||||
ENV LDFLAGS="-s -w -X github.com/tarampampam/error-pages/internal/version.version=$APP_VERSION"
|
||||
ENV LDFLAGS="-s -w -X gh.tarampamp.am/error-pages/internal/version.version=$APP_VERSION"
|
||||
|
||||
RUN set -x \
|
||||
&& go version \
|
||||
&& CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/ \
|
||||
&& ./error-pages version \
|
||||
&& ./error-pages --version \
|
||||
&& ./error-pages -h
|
||||
|
||||
WORKDIR /tmp/rootfs
|
||||
@ -31,13 +31,14 @@ 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
|
||||
|
||||
# generate static error pages (for usage inside another docker images, for example)
|
||||
RUN set -x \
|
||||
&& ./../bin/error-pages --config-file ./error-pages.yml build ./html --verbose --index \
|
||||
&& ./../bin/error-pages --verbose build --config-file ./error-pages.yml --index ./html \
|
||||
&& ls -l ./html
|
||||
|
||||
# use empty filesystem
|
||||
@ -59,18 +60,20 @@ 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"
|
||||
TEMPLATE_NAME="ghost" \
|
||||
DEFAULT_ERROR_PAGE="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", "--log-json", "healthcheck"]
|
||||
|
||||
ENTRYPOINT ["/bin/error-pages"]
|
||||
|
||||
CMD ["serve", "--log-json"]
|
||||
CMD ["--log-json", "serve"]
|
||||
|
9
Makefile
9
Makefile
@ -3,13 +3,13 @@
|
||||
# Makefile readme (en): <https://www.gnu.org/software/make/manual/html_node/index.html#SEC_Contents>
|
||||
|
||||
SHELL = /bin/sh
|
||||
LDFLAGS = "-s -w -X github.com/tarampampam/error-pages/internal/version.version=$(shell git rev-parse HEAD)"
|
||||
LDFLAGS = "-s -w -X gh.tarampamp.am/error-pages/internal/version.version=$(shell git rev-parse HEAD)"
|
||||
|
||||
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 ./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
|
||||
|
473
README.md
473
README.md
@ -1,358 +1,205 @@
|
||||
<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/actions/workflow/status/tarampampam/error-pages/tests.yml?branch=master&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/actions/workflow/status/tarampampam/error-pages/release.yml?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! ⚡<br/>
|
||||
10 apr. 2023 - ⚡ <strong>Two million times</strong> from the docker hub and <strong>one million</strong> from the ghcr! ⚡
|
||||
</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:
|
||||
|
||||
[][link_docker_hub]
|
||||
|
||||
Registry | Image
|
||||
-------------------------------------- | -----
|
||||
[Docker Hub][link_docker_hub] | `tarampampam/error-pages`
|
||||
[GitHub Container Registry][link_ghcr] | `ghcr.io/tarampampam/error-pages`
|
||||
| Registry | Image |
|
||||
|-----------------------------------|-----------------------------------|
|
||||
| [Docker Hub][docker-hub] | `tarampampam/error-pages` |
|
||||
| [GitHub Container Registry][ghcr] | `ghcr.io/tarampampam/error-pages` |
|
||||
|
||||
> Using the `latest` tag for the docker image is highly discouraged because of possible backward-incompatible changes during **major** upgrades. Please, use tags in `X.Y.Z` format
|
||||
|
||||
To watch the docker image content you can use the [dive](https://github.com/wagoodman/dive):
|
||||
💣 **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
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
All of the examples below will use a docker image with the application, but you can also use a binary. By the way, our docker image uses the **unleveled user** by default and **distroless**.
|
||||
|
||||
### HTTP server
|
||||
|
||||
As mentioned above - our application can be run as an HTTP server. It only needs to specify the path to the configuration file (it does not need statically generated error pages). The server uses [FastHTTP][fasthttp] and stores all necessary data in memory - so it does not use the file system and very fast. Oh yes, the image with the app also contains a configured **healthcheck** and **logs in JSON** format :)
|
||||
|
||||
For the HTTP server running execute in your terminal:
|
||||
|
||||
```bash
|
||||
$ docker run --rm \
|
||||
-p "8080:8080/tcp" \
|
||||
-e "TEMPLATE_NAME=random" \
|
||||
tarampampam/error-pages
|
||||
```
|
||||
|
||||
And open [`http://127.0.0.1:8080/404.html`](http://127.0.0.1:8080/404.html) in your favorite browser. Error pages are accessible by the following URLs: `http://127.0.0.1:8080/{page_code}.html`.
|
||||
|
||||
Environment variable `TEMPLATE_NAME` should be used for the theme switching (supported templates are described below).
|
||||
|
||||
> **Cheat**: you can use `random` (to use the randomized theme on server start) or `i-said-random` (to use the randomized template on **each request**)
|
||||
|
||||
To see the help run the following command:
|
||||
|
||||
```bash
|
||||
$ docker run --rm tarampampam/error-pages serve --help
|
||||
```
|
||||
|
||||
### Generator
|
||||
|
||||
Create a config file (`error-pages.yml`) with the following content:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
- path: ./foo.html # Template name "foo" (same as file name),
|
||||
# content located in the file "foo.html"
|
||||
- name: bar # Template name "bar", its content is described below:
|
||||
content: "Error {{ code }}: {{ message }} ({{ description }})"
|
||||
|
||||
pages:
|
||||
400:
|
||||
message: Bad Request
|
||||
description: The server did not understand the request
|
||||
|
||||
401:
|
||||
message: Unauthorized
|
||||
description: The requested page needs a username and a password
|
||||
```
|
||||
|
||||
Template file `foo.html`:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<title>{{ code }}</title>
|
||||
<body>
|
||||
<h1>{{ message }}: {{ description }}</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
And run the generator:
|
||||
|
||||
```bash
|
||||
$ docker run --rm \
|
||||
-v "$(pwd):/opt:rw" \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
tarampampam/error-pages build --config-file ./error-pages.yml ./out
|
||||
|
||||
$ tree
|
||||
.
|
||||
├── error-pages.yml
|
||||
├── foo.html
|
||||
└── out
|
||||
├── bar
|
||||
│ ├── 400.html
|
||||
│ └── 401.html
|
||||
└── foo
|
||||
├── 400.html
|
||||
└── 401.html
|
||||
|
||||
3 directories, 6 files
|
||||
|
||||
$ cat ./out/foo/400.html
|
||||
<html>
|
||||
<title>400</title>
|
||||
<body>
|
||||
<h1>Bad Request: The server did not understand the request</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
$ cat ./out/bar/400.html
|
||||
Error 400: Bad Request (The server did not understand the request)
|
||||
```
|
||||
|
||||
To see the usage help run the following command:
|
||||
|
||||
```bash
|
||||
$ docker run --rm tarampampam/error-pages build --help
|
||||
```
|
||||
|
||||
### Static error pages
|
||||
|
||||
You may want to use the generated error pages somewhere else, and you can simply extract them from the docker image to your local directory for this purpose:
|
||||
|
||||
```bash
|
||||
$ docker create --name error-pages tarampampam/error-pages:2.0.0-rc2
|
||||
$ docker cp error-pages:/opt/html ./out
|
||||
$ docker rm -f error-pages
|
||||
$ ls ./out
|
||||
ghost hacker-terminal index.html l7-dark l7-light noise shuffle
|
||||
$ tree
|
||||
.
|
||||
└── out
|
||||
├── ghost
|
||||
│ ├── 400.html
|
||||
│ ├── ...
|
||||
│ └── 505.html
|
||||
├── hacker-terminal
|
||||
│ ├── 400.html
|
||||
│ ├── ...
|
||||
│ └── 505.html
|
||||
├── index.html
|
||||
├── l7-dark
|
||||
│ ├── 400.html
|
||||
│ ├── ...
|
||||
...
|
||||
```
|
||||
|
||||
### Custom error pages for your image with [nginx][link_nginx]
|
||||
|
||||
You can build your own docker image with `nginx` and our error pages:
|
||||
|
||||
```nginx
|
||||
# File `nginx.conf`
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
error_page 401 /_error-pages/401.html;
|
||||
error_page 403 /_error-pages/403.html;
|
||||
error_page 404 /_error-pages/404.html;
|
||||
error_page 500 /_error-pages/500.html;
|
||||
error_page 502 /_error-pages/502.html;
|
||||
error_page 503 /_error-pages/503.html;
|
||||
|
||||
location ^~ /_error-pages/ {
|
||||
internal;
|
||||
root /usr/share/nginx/errorpages;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dockerfile
|
||||
# File `Dockerfile`
|
||||
|
||||
FROM nginx:1.21-alpine
|
||||
|
||||
COPY --chown=nginx \
|
||||
./nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --chown=nginx \
|
||||
--from=tarampampam/error-pages:2.0.0 \
|
||||
/opt/html/ghost /usr/share/nginx/errorpages/_error-pages
|
||||
```
|
||||
|
||||
```shell
|
||||
$ docker build --tag your-nginx:local -f ./Dockerfile .
|
||||
```
|
||||
|
||||
> More info about `error_page` directive can be [found here](http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page).
|
||||
|
||||
## Templates
|
||||
|
||||
Name | Preview
|
||||
:---------------: | :-----:
|
||||
`ghost` | [](https://tarampampam.github.io/error-pages/ghost/404.html)
|
||||
`l7-light` | [](https://tarampampam.github.io/error-pages/l7-light/404.html)
|
||||
`l7-dark` | [](https://tarampampam.github.io/error-pages/l7-dark/404.html)
|
||||
`shuffle` | [](https://tarampampam.github.io/error-pages/shuffle/404.html)
|
||||
`noise` | [](https://tarampampam.github.io/error-pages/noise/404.html)
|
||||
`hacker-terminal` | [](https://tarampampam.github.io/error-pages/hacker-terminal/404.html)
|
||||
| Name | Preview |
|
||||
|:-----------------:|:------------------------------------------------------------------:|
|
||||
| `ghost` | [![ghost][ghost-screen]][ghost-link] |
|
||||
| `l7-light` | [![l7-light][l7-light-screen]][l7-light-link] |
|
||||
| `l7-dark` | [![l7-dark][l7-dark-screen]][l7-dark-link] |
|
||||
| `shuffle` | [![shuffle][shuffle-screen]][shuffle-link] |
|
||||
| `noise` | [![noise][noise-screen]][noise-link] |
|
||||
| `hacker-terminal` | [![hacker-terminal][hacker-terminal-screen]][hacker-terminal-link] |
|
||||
| `cats` | [![cats][cats-screen]][cats-link] |
|
||||
| `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] |
|
||||
| `orient` | [![orient][orient-screen]][orient-link] |
|
||||
|
||||
> Note: `noise` template highly uses the CPU, be careful
|
||||
|
||||
## Custom error pages for [Traefik][link_traefik]
|
||||
[ghost-screen]:https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif
|
||||
[ghost-link]:https://tarampampam.github.io/error-pages/ghost/404.html
|
||||
[l7-light-screen]:https://hsto.org/webt/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
|
||||
[orient-screen]:https://hsto.org/webt/pz/eu/v_/pzeuv_lyeqr0xpusa4zfrtgk7sa.png
|
||||
[orient-link]:https://tarampampam.github.io/error-pages/orient/404.html
|
||||
|
||||
Simple traefik (tested on `v2.5.3`) service configuration for usage in [docker swarm][link_swarm] (**change with your needs**):
|
||||
## 🦾 Contributors
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
I want to say a big thank you to everyone who contributed to this project:
|
||||
|
||||
services:
|
||||
error-pages:
|
||||
image: tarampampam/error-pages:2.0.0
|
||||
environment:
|
||||
TEMPLATE_NAME: l7-dark
|
||||
networks:
|
||||
- traefik-public
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == worker
|
||||
labels:
|
||||
traefik.enable: 'true'
|
||||
traefik.docker.network: traefik-public
|
||||
# use as "fallback" for any non-registered services (with priority below normal)
|
||||
traefik.http.routers.error-pages-router.rule: HostRegexp(`{host:.+}`)
|
||||
traefik.http.routers.error-pages-router.priority: 10
|
||||
# should say that all of your services work on https
|
||||
traefik.http.routers.error-pages-router.tls: 'true'
|
||||
traefik.http.routers.error-pages-router.entrypoints: https
|
||||
traefik.http.routers.error-pages-router.middlewares: error-pages-middleware@docker
|
||||
traefik.http.services.error-pages-service.loadbalancer.server.port: 8080
|
||||
# "errors" middleware settings
|
||||
traefik.http.middlewares.error-pages-middleware.errors.status: 400-599
|
||||
traefik.http.middlewares.error-pages-middleware.errors.service: error-pages-service@docker
|
||||
traefik.http.middlewares.error-pages-middleware.errors.query: /{status}.html
|
||||
[][contributors]
|
||||
|
||||
any-another-http-service:
|
||||
image: nginx:alpine
|
||||
networks:
|
||||
- traefik-public
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.role == worker
|
||||
labels:
|
||||
traefik.enable: 'true'
|
||||
traefik.docker.network: traefik-public
|
||||
traefik.http.routers.another-service.rule: Host(`subdomain.example.com`)
|
||||
traefik.http.routers.another-service.tls: 'true'
|
||||
traefik.http.routers.another-service.entrypoints: https
|
||||
# next line is important
|
||||
traefik.http.routers.another-service.middlewares: error-pages-middleware@docker
|
||||
traefik.http.services.another-service.loadbalancer.server.port: 80
|
||||
[contributors]:https://github.com/tarampampam/error-pages/graphs/contributors
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
```
|
||||
## 📰 Changes log
|
||||
|
||||
## Changes log
|
||||
[![Release date][badge-release-date]][releases]
|
||||
[![Commits since latest release][badge-commits]][commits]
|
||||
|
||||
[![Release date][badge_release_date]][link_releases]
|
||||
[![Commits since latest release][badge_commits_since_release]][link_commits]
|
||||
Changes log can be [found here][changelog].
|
||||
|
||||
Changes log can be [found here][link_changes_log].
|
||||
## 👾 Support
|
||||
|
||||
## Support
|
||||
[![Issues][badge-issues]][issues]
|
||||
[![Issues][badge-prs]][prs]
|
||||
|
||||
[![Issues][badge_issues]][link_issues]
|
||||
[![Issues][badge_pulls]][link_pulls]
|
||||
If you find any bugs in the project, please [create an issue][new-issue] in the current repository.
|
||||
|
||||
If you will find any package errors, please, [make an issue][link_create_issue] in current repository.
|
||||
## 📖 License
|
||||
|
||||
## License
|
||||
This is open-sourced software licensed under the [MIT License][license].
|
||||
|
||||
This is open-sourced software licensed under the [MIT License][link_license].
|
||||
[badge-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
|
||||
|
||||
[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
|
||||
|
@ -5,9 +5,14 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/tarampampam/error-pages/internal/cli"
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/cli"
|
||||
)
|
||||
|
||||
// set GOMAXPROCS to match Linux container CPU quota.
|
||||
var _, _ = maxprocs.Set(maxprocs.Min(1), maxprocs.Logger(func(_ string, _ ...any) {}))
|
||||
|
||||
// exitFn is a function for application exiting.
|
||||
var exitFn = os.Exit //nolint:gochecknoglobals
|
||||
|
||||
@ -17,9 +22,7 @@ func main() { exitFn(run()) }
|
||||
// run this CLI application.
|
||||
// Exit codes documentation: <https://tldp.org/LDP/abs/html/exitcodes.html>
|
||||
func run() int {
|
||||
cmd := cli.NewCommand(filepath.Base(os.Args[0]))
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
if err := (cli.NewApp(filepath.Base(os.Args[0]))).Run(os.Args); err != nil {
|
||||
_, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error())
|
||||
|
||||
return 1
|
||||
|
21
cmd/error-pages/main_test.go
Normal file
21
cmd/error-pages/main_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_MainHelp(t *testing.T) {
|
||||
os.Args = []string{"", "--help"}
|
||||
exitFn = func(code int) { require.Equal(t, 0, code) }
|
||||
|
||||
output := capturer.CaptureStdout(main)
|
||||
|
||||
assert.Contains(t, output, "USAGE:")
|
||||
assert.Contains(t, output, "COMMANDS:")
|
||||
assert.Contains(t, output, "GLOBAL OPTIONS:")
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
# Docker-compose file is used only for local development. This is not production-ready example.
|
||||
|
||||
version: '3.4'
|
||||
version: '3.8'
|
||||
|
||||
volumes:
|
||||
tmp-data: {}
|
||||
golint-go: {}
|
||||
golint-cache: {}
|
||||
|
||||
services:
|
||||
app: &app-service
|
||||
image: golang:1.17.1-buster # Image page: <https://hub.docker.com/_/golang>
|
||||
image: golang:1.21-buster # Image page: <https://hub.docker.com/_/golang>
|
||||
working_dir: /src
|
||||
environment:
|
||||
HOME: /tmp
|
||||
@ -18,29 +19,35 @@ services:
|
||||
- /etc/group:/etc/group:ro
|
||||
- .:/src:rw
|
||||
- tmp-data:/tmp:rw
|
||||
security_opt: [no-new-privileges:true]
|
||||
|
||||
web:
|
||||
<<: *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 -buildvcs=false -o /tmp/app ./cmd/error-pages && /tmp/app serve --show-details --proxy-headers=X-Foo,Bar,Baz_blah --catch-all"
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/health/live']
|
||||
interval: 5s
|
||||
timeout: 2s
|
||||
test: ['CMD', '/tmp/app', '--log-json', 'healthcheck']
|
||||
interval: 4s
|
||||
start_period: 5s
|
||||
retries: 5
|
||||
|
||||
golint:
|
||||
image: golangci/golangci-lint:v1.42-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint>
|
||||
image: golangci/golangci-lint:v1.54-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint>
|
||||
environment:
|
||||
GOLANGCI_LINT_CACHE: /tmp/golint # <https://github.com/golangci/golangci-lint/blob/v1.42.0/internal/cache/default.go#L68>
|
||||
volumes:
|
||||
- .:/src:ro
|
||||
- golint-go:/go:rw # go dependencies will be downloaded on each run without this
|
||||
- golint-cache:/tmp/golint:rw
|
||||
- .:/src:ro
|
||||
working_dir: /src
|
||||
command: /bin/true
|
||||
security_opt: [no-new-privileges:true]
|
||||
|
||||
hurl:
|
||||
image: orangeopensource/hurl:1.8.0
|
||||
volumes:
|
||||
- .:/src:ro
|
||||
working_dir: /src
|
||||
depends_on:
|
||||
web: {condition: service_healthy}
|
||||
security_opt: [no-new-privileges:true]
|
||||
|
@ -10,6 +10,53 @@ templates:
|
||||
- path: ./templates/shuffle.html
|
||||
- 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
|
||||
- path: ./templates/orient.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:
|
||||
|
54
go.mod
54
go.mod
@ -1,32 +1,44 @@
|
||||
module github.com/tarampampam/error-pages
|
||||
module gh.tarampamp.am/error-pages
|
||||
|
||||
go 1.17
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/a8m/envsubst v1.2.0
|
||||
github.com/fasthttp/router v1.4.3
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/a8m/envsubst v1.4.2
|
||||
github.com/fasthttp/router v1.4.20
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/valyala/fasthttp v1.30.0
|
||||
go.uber.org/zap v1.19.1
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/prometheus/client_model v0.4.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
github.com/valyala/fasthttp v1.49.0
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
go.uber.org/goleak v1.2.1
|
||||
go.uber.org/zap v1.25.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.3 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/klauspost/compress v1.16.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
)
|
||||
|
669
go.sum
669
go.sum
@ -1,615 +1,86 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
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/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=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/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=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/a8m/envsubst v1.2.0 h1:yvzAhJD2QKdo35Ut03wIfXQmg+ta3wC/1bskfZynz+Q=
|
||||
github.com/a8m/envsubst v1.2.0/go.mod h1:PpvLvNWa+Rvu/10qXmFbFiGICIU5hZvFJNPCCkUaObg=
|
||||
github.com/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-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/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/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/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg=
|
||||
github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fasthttp/router v1.4.3 h1:spS+LUnRryQ/+hbmYzs/xWGJlQCkeQI3hxGZdlVYhLU=
|
||||
github.com/fasthttp/router v1.4.3/go.mod h1:9ytWCfZ5LcCcbD3S7pEXyBX9vZnOZmN918WiiaYUzr8=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
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/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=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
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/fasthttp/router v1.4.20 h1:yPeNxz5WxZGojzolKqiP15DTXnxZce9Drv577GBrDgU=
|
||||
github.com/fasthttp/router v1.4.20/go.mod h1:um867yNQKtERxBm+C+yzgWxjspTiQoA8z86Ec3fK/tc=
|
||||
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
|
||||
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
|
||||
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=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
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/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=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/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/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=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/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/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
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/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
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/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/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/savsgio/gotils v0.0.0-20210907153846-c06938798b52/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
|
||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8=
|
||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
|
||||
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
|
||||
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g=
|
||||
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
go.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.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/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=
|
||||
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-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/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=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
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=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-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-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=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
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-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
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-20210402161424-2e8d93401602/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=
|
||||
github.com/valyala/fasthttp v1.49.0 h1:9FdvCpmxB74LH4dPb7IJ1cOSsluR07XG3I1txXWwJpE=
|
||||
github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
|
||||
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20181026203630-95b1ffbd15a5/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-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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/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=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-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-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-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 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
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-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
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.2/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/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
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.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||
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=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
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-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
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=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
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.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
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=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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.8/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=
|
||||
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=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// OSSignals allows to subscribe for system signals.
|
||||
// OSSignals allows subscribing for system signals.
|
||||
type OSSignals struct {
|
||||
ctx context.Context
|
||||
ch chan os.Signal
|
||||
@ -22,7 +22,7 @@ func NewOSSignals(ctx context.Context) OSSignals {
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe for some of system signals (call Stop for stopping).
|
||||
// Subscribe for some system signals (call Stop for stopping).
|
||||
func (oss *OSSignals) Subscribe(onSignal func(os.Signal), signals ...os.Signal) {
|
||||
if len(signals) == 0 {
|
||||
signals = []os.Signal{os.Interrupt, syscall.SIGINT, syscall.SIGTERM} // default signals
|
||||
|
@ -8,7 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/breaker"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/breaker"
|
||||
)
|
||||
|
||||
func TestNewOSSignals(t *testing.T) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -3,12 +3,13 @@ package checkers_test
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/checkers"
|
||||
)
|
||||
|
||||
type httpClientFunc func(*http.Request) (*http.Response, error)
|
||||
@ -18,11 +19,11 @@ 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{
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte{})),
|
||||
StatusCode: http.StatusOK,
|
||||
}, nil
|
||||
}
|
||||
@ -35,7 +36,7 @@ func TestHealthChecker_CheckSuccess(t *testing.T) {
|
||||
func TestHealthChecker_CheckFail(t *testing.T) {
|
||||
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte{})),
|
||||
StatusCode: http.StatusBadGateway,
|
||||
}, nil
|
||||
}
|
||||
|
@ -4,7 +4,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/checkers"
|
||||
)
|
||||
|
||||
func TestLiveChecker_Check(t *testing.T) {
|
||||
|
103
internal/cli/app.go
Normal file
103
internal/cli/app.go
Normal file
@ -0,0 +1,103 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/checkers"
|
||||
"gh.tarampamp.am/error-pages/internal/cli/build"
|
||||
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
|
||||
"gh.tarampamp.am/error-pages/internal/cli/serve"
|
||||
"gh.tarampamp.am/error-pages/internal/env"
|
||||
"gh.tarampamp.am/error-pages/internal/logger"
|
||||
"gh.tarampamp.am/error-pages/internal/version"
|
||||
)
|
||||
|
||||
// NewApp creates new console application.
|
||||
func NewApp(appName string) *cli.App { //nolint:funlen
|
||||
const (
|
||||
logLevelFlagName = "log-level"
|
||||
logFormatFlagName = "log-format"
|
||||
verboseFlagName = "verbose"
|
||||
debugFlagName = "debug"
|
||||
logJSONFlagName = "log-json"
|
||||
|
||||
defaultLogLevel = logger.InfoLevel
|
||||
defaultLogFormat = logger.ConsoleFormat
|
||||
)
|
||||
|
||||
// create "default" logger (will be overwritten later with customized)
|
||||
var log, _ = logger.New(defaultLogLevel, defaultLogFormat) // error will never occurs
|
||||
|
||||
return &cli.App{
|
||||
Usage: appName,
|
||||
Before: func(c *cli.Context) (err error) {
|
||||
_ = log.Sync() // sync previous logger instance
|
||||
|
||||
var logLevel, logFormat = defaultLogLevel, defaultLogFormat //nolint:ineffassign
|
||||
|
||||
if c.Bool(verboseFlagName) || c.Bool(debugFlagName) {
|
||||
logLevel = logger.DebugLevel
|
||||
} else {
|
||||
// parse logging level
|
||||
if logLevel, err = logger.ParseLevel(c.String(logLevelFlagName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool(logJSONFlagName) {
|
||||
logFormat = logger.JSONFormat
|
||||
} else {
|
||||
// parse logging format
|
||||
if logFormat, err = logger.ParseFormat(c.String(logFormatFlagName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
configured, err := logger.New(logLevel, logFormat) // create new logger instance
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*log = *configured // replace "default" logger with customized
|
||||
|
||||
return nil
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
healthcheck.NewCommand(checkers.NewHealthChecker(context.TODO())),
|
||||
build.NewCommand(log),
|
||||
serve.NewCommand(log),
|
||||
},
|
||||
Version: fmt.Sprintf("%s (%s)", version.Version(), runtime.Version()),
|
||||
Flags: []cli.Flag{ // global flags
|
||||
&cli.BoolFlag{ // kept for backward compatibility
|
||||
Name: verboseFlagName,
|
||||
Usage: "verbose output (DEPRECATED FLAG)",
|
||||
},
|
||||
&cli.BoolFlag{ // kept for backward compatibility
|
||||
Name: debugFlagName,
|
||||
Usage: "debug output (DEPRECATED FLAG)",
|
||||
},
|
||||
&cli.BoolFlag{ // kept for backward compatibility
|
||||
Name: logJSONFlagName,
|
||||
Usage: "logs in JSON format (DEPRECATED FLAG)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logLevelFlagName,
|
||||
Value: defaultLogLevel.String(),
|
||||
Usage: "logging level (`" + strings.Join(logger.LevelStrings(), "/") + "`)",
|
||||
EnvVars: []string{env.LogLevel.String()},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: logFormatFlagName,
|
||||
Value: defaultLogFormat.String(),
|
||||
Usage: "logging format (`" + strings.Join(logger.FormatStrings(), "/") + "`)",
|
||||
EnvVars: []string{env.LogFormat.String()},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
19
internal/cli/app_test.go
Normal file
19
internal/cli/app_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/cli"
|
||||
)
|
||||
|
||||
func TestNewCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := cli.NewApp("app")
|
||||
|
||||
assert.NotEmpty(t, app.Flags)
|
||||
|
||||
assert.NoError(t, app.Run([]string{"", "--log-level", "debug", "--log-format", "json"}))
|
||||
}
|
@ -1,141 +1,147 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/cli/shared"
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
type historyItem struct {
|
||||
Code, Message, Path string
|
||||
type command struct {
|
||||
c *cli.Command
|
||||
}
|
||||
|
||||
// NewCommand creates `build` command.
|
||||
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:funlen,gocognit
|
||||
var (
|
||||
generateIndex bool
|
||||
cfg *config.Config
|
||||
func NewCommand(log *zap.Logger) *cli.Command {
|
||||
var cmd = command{}
|
||||
|
||||
const (
|
||||
generateIndexFlagName = "index"
|
||||
disableL10nFlagName = "disable-l10n"
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "build <output-directory>",
|
||||
Aliases: []string{"b"},
|
||||
Short: "Build the error pages",
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(*cobra.Command, []string) error {
|
||||
if configFile == nil {
|
||||
return errors.New("path to the config file is required for this command")
|
||||
cmd.c = &cli.Command{
|
||||
Name: "build",
|
||||
Aliases: []string{"b"},
|
||||
Usage: "build <output-directory>",
|
||||
Description: "Build the error pages",
|
||||
Action: func(c *cli.Context) error {
|
||||
cfg, cfgErr := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name))
|
||||
if cfgErr != nil {
|
||||
return cfgErr
|
||||
}
|
||||
|
||||
if c, err := config.FromYamlFile(*configFile); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err = c.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg = c
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
if c.Args().Len() != 1 {
|
||||
return errors.New("wrong arguments count")
|
||||
}
|
||||
|
||||
log.Info("loading templates")
|
||||
|
||||
templates, err := cfg.LoadTemplates()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(templates) == 0 {
|
||||
return errors.New("no loaded templates")
|
||||
}
|
||||
|
||||
log.Debug("the output directory preparing", zap.String("Path", args[0]))
|
||||
|
||||
if err = createDirectory(args[0]); err != nil {
|
||||
return errors.Wrap(err, "cannot prepare output directory")
|
||||
}
|
||||
|
||||
codes := make(map[string]tpl.Annotator)
|
||||
|
||||
for code, desc := range cfg.Pages {
|
||||
codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
|
||||
}
|
||||
|
||||
history := make(map[string][]historyItem, len(templates))
|
||||
|
||||
log.Info("saving the error pages")
|
||||
startedAt := time.Now()
|
||||
|
||||
if err = tpl.NewErrors(templates, codes).VisitAll(func(template, code string, content []byte) error {
|
||||
if e := createDirectory(path.Join(args[0], template)); e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
fileName := code + ".html"
|
||||
|
||||
if e := os.WriteFile(path.Join(args[0], template, fileName), content, 0664); e != nil { //nolint:gosec,gomnd
|
||||
return e
|
||||
}
|
||||
|
||||
if _, ok := history[template]; !ok {
|
||||
history[template] = make([]historyItem, 0, len(codes))
|
||||
}
|
||||
|
||||
history[template] = append(history[template], historyItem{
|
||||
Code: code,
|
||||
Message: codes[code].Message,
|
||||
Path: path.Join(template, fileName),
|
||||
})
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("saved", zap.Duration("duration", time.Since(startedAt)))
|
||||
|
||||
if generateIndex {
|
||||
log.Info("index file generation")
|
||||
startedAt = time.Now()
|
||||
|
||||
if err = writeIndexFile(path.Join(args[0], "index.html"), history); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("index file generated", zap.Duration("duration", time.Since(startedAt)))
|
||||
}
|
||||
|
||||
return nil
|
||||
return cmd.Run(log, cfg, c.Args().First(), c.Bool(generateIndexFlagName), c.Bool(disableL10nFlagName))
|
||||
},
|
||||
Flags: []cli.Flag{ // global flags
|
||||
&cli.BoolFlag{
|
||||
Name: generateIndexFlagName,
|
||||
Aliases: []string{"i"},
|
||||
Usage: "generate index page",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: disableL10nFlagName,
|
||||
Usage: "disable error pages localization",
|
||||
},
|
||||
shared.ConfigFileFlag,
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(
|
||||
&generateIndex,
|
||||
"index", "i",
|
||||
false,
|
||||
"generate index page",
|
||||
)
|
||||
|
||||
return cmd
|
||||
return cmd.c
|
||||
}
|
||||
|
||||
func createDirectory(path string) error {
|
||||
const (
|
||||
outHTMLFileExt = ".html"
|
||||
outIndexFileName = "index"
|
||||
outFilePerm = os.FileMode(0664)
|
||||
outDirPerm = os.FileMode(0775)
|
||||
)
|
||||
|
||||
func (cmd *command) 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 := cmd.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 := cmd.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 (cmd *command) createDirectory(path string, perm os.FileMode) error {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(path, 0775) //nolint:gomnd
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
||||
|
||||
return err
|
||||
@ -147,52 +153,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
|
||||
}
|
||||
|
@ -1,7 +1,26 @@
|
||||
package build_test
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/goleak"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/cli/build"
|
||||
)
|
||||
|
||||
func TestNewCommand(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
|
||||
cmd := build.NewCommand(zap.NewNop())
|
||||
|
||||
assert.NotEmpty(t, cmd.Flags)
|
||||
|
||||
assert.Error(t, cmd.Run(
|
||||
cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil),
|
||||
"",
|
||||
), "should fail because of missing external services")
|
||||
}
|
||||
|
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://gh.tarampamp.am/error-pages">project repository</a>.
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -2,56 +2,35 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/cli/shared"
|
||||
)
|
||||
|
||||
type checker interface {
|
||||
Check(port uint16) error
|
||||
}
|
||||
|
||||
const portFlagName = "port"
|
||||
|
||||
// NewCommand creates `healthcheck` command.
|
||||
func NewCommand(checker checker) *cobra.Command {
|
||||
var port uint16
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "healthcheck",
|
||||
func NewCommand(checker checker) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "healthcheck",
|
||||
Aliases: []string{"chk", "health", "check"},
|
||||
Short: "Health checker for the HTTP server. Use case - docker healthcheck",
|
||||
PreRunE: func(c *cobra.Command, _ []string) (lastErr error) {
|
||||
c.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
// flag was NOT defined using CLI (flags should have maximal priority)
|
||||
if !flag.Changed && flag.Name == portFlagName {
|
||||
if envPort, exists := env.ListenPort.Lookup(); exists && envPort != "" {
|
||||
if p, err := strconv.ParseUint(envPort, 10, 16); err == nil { //nolint:gomnd
|
||||
port = uint16(p)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Usage: "Health checker for the HTTP server. Use case - docker healthcheck",
|
||||
Action: func(c *cli.Context) error {
|
||||
var port = c.Uint(shared.ListenPortFlag.Name)
|
||||
|
||||
return lastErr
|
||||
if port <= 0 || port > math.MaxUint16 {
|
||||
return errors.New("port value out of range")
|
||||
}
|
||||
|
||||
return checker.Check(uint16(port))
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
return checker.Check(port)
|
||||
Flags: []cli.Flag{
|
||||
shared.ListenPortFlag,
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Uint16VarP(
|
||||
&port,
|
||||
portFlagName,
|
||||
"p",
|
||||
8080, //nolint:gomnd // must be same as default serve `--port` flag value
|
||||
fmt.Sprintf("TCP port number [$%s]", env.ListenPort),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ package healthcheck_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/cli/healthcheck"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
|
||||
)
|
||||
|
||||
type fakeChecker struct{ err error }
|
||||
@ -18,77 +18,30 @@ func (c *fakeChecker) Check(port uint16) error { return c.err }
|
||||
func TestProperties(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
|
||||
assert.Equal(t, "healthcheck", cmd.Use)
|
||||
assert.Equal(t, "healthcheck", cmd.Name)
|
||||
assert.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases)
|
||||
assert.NotNil(t, cmd.RunE)
|
||||
assert.NotNil(t, cmd.Action)
|
||||
}
|
||||
|
||||
func TestCommandRun(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureOutput(func() {
|
||||
assert.NoError(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Empty(t, output)
|
||||
assert.NoError(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)))
|
||||
}
|
||||
|
||||
func TestCommandRunFailed(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: errors.New("foo err")})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "foo err")
|
||||
assert.ErrorContains(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)), "foo err")
|
||||
}
|
||||
|
||||
func TestPortFlagWrongArgument(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{"-p", "65536"}) // 65535 is max
|
||||
|
||||
var executed bool
|
||||
err := cmd.Run(
|
||||
cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil),
|
||||
"", "-p", "65536",
|
||||
)
|
||||
|
||||
cmd.RunE = func(*cobra.Command, []string) error {
|
||||
executed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "invalid argument")
|
||||
assert.Contains(t, output, "65536")
|
||||
assert.Contains(t, output, "value out of range")
|
||||
assert.False(t, executed)
|
||||
}
|
||||
|
||||
func TestPortFlagWrongEnvValue(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
assert.NoError(t, os.Setenv("LISTEN_PORT", "65536")) // 65535 is max
|
||||
|
||||
defer func() { assert.NoError(t, os.Unsetenv("LISTEN_PORT")) }()
|
||||
|
||||
var executed bool
|
||||
|
||||
cmd.RunE = func(*cobra.Command, []string) error {
|
||||
executed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "wrong TCP port")
|
||||
assert.Contains(t, output, "environment variable")
|
||||
assert.Contains(t, output, "65536")
|
||||
assert.False(t, executed)
|
||||
assert.ErrorContains(t, err, "port value out of range")
|
||||
}
|
||||
|
@ -1,93 +0,0 @@
|
||||
// Package cli contains CLI command handlers.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
buildCmd "github.com/tarampampam/error-pages/internal/cli/build"
|
||||
healthcheckCmd "github.com/tarampampam/error-pages/internal/cli/healthcheck"
|
||||
serveCmd "github.com/tarampampam/error-pages/internal/cli/serve"
|
||||
versionCmd "github.com/tarampampam/error-pages/internal/cli/version"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
"github.com/tarampampam/error-pages/internal/version"
|
||||
)
|
||||
|
||||
const configFileFlagName = "config-file"
|
||||
|
||||
// NewCommand creates root command.
|
||||
func NewCommand(appName string) *cobra.Command { //nolint:funlen
|
||||
var (
|
||||
configFile string
|
||||
verbose bool
|
||||
debug bool
|
||||
logJSON bool
|
||||
)
|
||||
|
||||
ctx := context.Background() // main CLI context
|
||||
|
||||
// create "default" logger (will be overwritten later with customized)
|
||||
log, err := logger.New(false, false, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: appName,
|
||||
PersistentPreRunE: func(c *cobra.Command, _ []string) error {
|
||||
_ = log.Sync() // sync previous logger instance
|
||||
|
||||
customizedLog, e := logger.New(verbose, debug, logJSON)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
*log = *customizedLog // override "default" logger with customized
|
||||
|
||||
c.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
// flag was NOT defined using CLI (flags should have maximal priority)
|
||||
if !flag.Changed && flag.Name == configFileFlagName {
|
||||
if envConfigFile, exists := env.ConfigFilePath.Lookup(); exists && envConfigFile != "" {
|
||||
configFile = envConfigFile
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
PersistentPostRun: func(*cobra.Command, []string) {
|
||||
// error ignoring reasons:
|
||||
// - <https://github.com/uber-go/zap/issues/772>
|
||||
// - <https://github.com/uber-go/zap/issues/328>
|
||||
_ = log.Sync()
|
||||
},
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
CompletionOptions: cobra.CompletionOptions{
|
||||
DisableDefaultCmd: true,
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
cmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "debug output")
|
||||
cmd.PersistentFlags().BoolVarP(&logJSON, "log-json", "", false, "logs in JSON format")
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&configFile,
|
||||
configFileFlagName, "c",
|
||||
"./error-pages.yml",
|
||||
fmt.Sprintf("path to the config file [$%s]", env.ConfigFilePath),
|
||||
)
|
||||
|
||||
cmd.AddCommand(
|
||||
versionCmd.NewCommand(version.Version()),
|
||||
healthcheckCmd.NewCommand(checkers.NewHealthChecker(ctx)),
|
||||
buildCmd.NewCommand(log, &configFile),
|
||||
serveCmd.NewCommand(ctx, log, &configFile),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/cli"
|
||||
)
|
||||
|
||||
func TestSubcommands(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
|
||||
cases := []struct {
|
||||
giveName string
|
||||
}{
|
||||
{giveName: "build"},
|
||||
{giveName: "version"},
|
||||
{giveName: "healthcheck"},
|
||||
{giveName: "serve"},
|
||||
}
|
||||
|
||||
// get all existing subcommands and put into the map
|
||||
subcommands := make(map[string]*cobra.Command)
|
||||
for _, sub := range cmd.Commands() {
|
||||
subcommands[sub.Name()] = sub
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.giveName, func(t *testing.T) {
|
||||
if _, exists := subcommands[tt.giveName]; !exists {
|
||||
assert.Failf(t, "command not found", "command [%s] was not found", tt.giveName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlags(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
|
||||
cases := []struct {
|
||||
giveName string
|
||||
wantShorthand string
|
||||
wantDefault string
|
||||
}{
|
||||
{giveName: "verbose", wantShorthand: "v", wantDefault: "false"},
|
||||
{giveName: "debug", wantShorthand: "", wantDefault: "false"},
|
||||
{giveName: "log-json", wantShorthand: "", wantDefault: "false"},
|
||||
{giveName: "config-file", wantShorthand: "c", wantDefault: "./error-pages.yml"},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.giveName, func(t *testing.T) {
|
||||
flag := cmd.Flag(tt.giveName)
|
||||
|
||||
if flag == nil {
|
||||
assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantShorthand, flag.Shorthand)
|
||||
assert.Equal(t, tt.wantDefault, flag.DefValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuting(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
var executed bool
|
||||
|
||||
if cmd.Run == nil { // override "Run" property for test (if it was not set)
|
||||
cmd.Run = func(cmd *cobra.Command, args []string) {
|
||||
executed = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.True(t, executed)
|
||||
}
|
@ -3,63 +3,171 @@ package serve
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tarampampam/error-pages/internal/breaker"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
appHttp "github.com/tarampampam/error-pages/internal/http"
|
||||
"gh.tarampamp.am/error-pages/internal/breaker"
|
||||
"gh.tarampamp.am/error-pages/internal/cli/shared"
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/env"
|
||||
appHttp "gh.tarampamp.am/error-pages/internal/http"
|
||||
"gh.tarampamp.am/error-pages/internal/options"
|
||||
"gh.tarampamp.am/error-pages/internal/pick"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
c *cli.Command
|
||||
}
|
||||
|
||||
const (
|
||||
templateNameFlagName = "template-name"
|
||||
defaultErrorPageFlagName = "default-error-page"
|
||||
defaultHTTPCodeFlagName = "default-http-code"
|
||||
showDetailsFlagName = "show-details"
|
||||
proxyHTTPHeadersFlagName = "proxy-headers"
|
||||
disableL10nFlagName = "disable-l10n"
|
||||
catchAllFlagName = "catch-all"
|
||||
)
|
||||
|
||||
const (
|
||||
useRandomTemplate = "random"
|
||||
useRandomTemplateOnEachRequest = "i-said-random"
|
||||
useRandomTemplateDaily = "random-daily"
|
||||
useRandomTemplateHourly = "random-hourly"
|
||||
)
|
||||
|
||||
// NewCommand creates `serve` command.
|
||||
func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command {
|
||||
var (
|
||||
f flags
|
||||
cfg *config.Config
|
||||
)
|
||||
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
|
||||
var cmd = command{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
cmd.c = &cli.Command{
|
||||
Name: "serve",
|
||||
Aliases: []string{"s", "server"},
|
||||
Short: "Start HTTP server",
|
||||
PreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if configFile == nil {
|
||||
Usage: "Start HTTP server",
|
||||
Action: func(c *cli.Context) error {
|
||||
var cfg *config.Config
|
||||
|
||||
if configPath := c.String(shared.ConfigFileFlag.Name); configPath == "" { // load config from file
|
||||
return errors.New("path to the config file is required for this command")
|
||||
}
|
||||
|
||||
if err := f.overrideUsingEnv(cmd.Flags()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c, err := config.FromYamlFile(*configFile); err != nil {
|
||||
} else if loadedCfg, err := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name)); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err = c.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg = c
|
||||
cfg = loadedCfg
|
||||
}
|
||||
|
||||
return f.validate()
|
||||
var (
|
||||
ip = c.String(shared.ListenAddrFlag.Name)
|
||||
port = uint16(c.Uint(shared.ListenPortFlag.Name))
|
||||
o options.ErrorPage
|
||||
)
|
||||
|
||||
if net.ParseIP(ip) == nil {
|
||||
return fmt.Errorf("wrong IP address [%s] for listening", ip)
|
||||
}
|
||||
|
||||
{ // fill options
|
||||
o.Template.Name = c.String(templateNameFlagName)
|
||||
o.L10n.Disabled = c.Bool(disableL10nFlagName)
|
||||
o.Default.PageCode = c.String(defaultErrorPageFlagName)
|
||||
o.Default.HTTPCode = uint16(c.Uint(defaultHTTPCodeFlagName))
|
||||
o.ShowDetails = c.Bool(showDetailsFlagName)
|
||||
o.CatchAll = c.Bool(catchAllFlagName)
|
||||
|
||||
if headers := c.String(proxyHTTPHeadersFlagName); headers != "" { //nolint:nestif
|
||||
var m = make(map[string]struct{})
|
||||
|
||||
// make unique and ignore empty strings
|
||||
for _, header := range strings.Split(headers, ",") {
|
||||
if h := strings.TrimSpace(header); h != "" {
|
||||
if strings.ContainsRune(h, ' ') {
|
||||
return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", header)
|
||||
}
|
||||
|
||||
if _, ok := m[h]; !ok {
|
||||
m[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert map into slice
|
||||
o.ProxyHTTPHeaders = make([]string, 0, len(m))
|
||||
for h := range m {
|
||||
o.ProxyHTTPHeaders = append(o.ProxyHTTPHeaders, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if o.Default.HTTPCode > 599 { //nolint:gomnd
|
||||
return fmt.Errorf("wrong default HTTP response code [%d]", o.Default.HTTPCode)
|
||||
}
|
||||
|
||||
return cmd.Run(c.Context, log, cfg, ip, port, o)
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
shared.ConfigFileFlag,
|
||||
shared.ListenPortFlag,
|
||||
shared.ListenAddrFlag,
|
||||
&cli.StringFlag{
|
||||
Name: templateNameFlagName,
|
||||
Aliases: []string{"t"},
|
||||
Usage: fmt.Sprintf(
|
||||
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on "+
|
||||
"each request or \"%s/%s\" daily/hourly randomized)",
|
||||
useRandomTemplate,
|
||||
useRandomTemplateOnEachRequest,
|
||||
useRandomTemplateDaily,
|
||||
useRandomTemplateHourly,
|
||||
),
|
||||
EnvVars: []string{env.TemplateName.String()},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: defaultErrorPageFlagName,
|
||||
Value: "404",
|
||||
Usage: "default error page",
|
||||
EnvVars: []string{env.DefaultErrorPage.String()},
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: defaultHTTPCodeFlagName,
|
||||
Value: 404, //nolint:gomnd
|
||||
Usage: "default HTTP response code",
|
||||
EnvVars: []string{env.DefaultHTTPCode.String()},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: showDetailsFlagName,
|
||||
Usage: "show request details in response",
|
||||
EnvVars: []string{env.ShowDetails.String()},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: proxyHTTPHeadersFlagName,
|
||||
Usage: "proxy HTTP request headers list (comma-separated)",
|
||||
EnvVars: []string{env.ProxyHTTPHeaders.String()},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: disableL10nFlagName,
|
||||
Usage: "disable error pages localization",
|
||||
EnvVars: []string{env.DisableL10n.String()},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: catchAllFlagName,
|
||||
Usage: "catch all pages",
|
||||
EnvVars: []string{env.CatchAll.String()},
|
||||
},
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error { return run(ctx, log, f, cfg) },
|
||||
}
|
||||
|
||||
f.init(cmd.Flags())
|
||||
|
||||
return cmd
|
||||
return cmd.c
|
||||
}
|
||||
|
||||
const serverShutdownTimeout = 15 * time.Second
|
||||
|
||||
// run current command.
|
||||
func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) error { //nolint:funlen
|
||||
// Run current command.
|
||||
func (cmd *command) Run( //nolint:funlen
|
||||
parentCtx context.Context, log *zap.Logger, cfg *config.Config, ip string, port uint16, opt options.ErrorPage,
|
||||
) error {
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
|
||||
oss = breaker.NewOSSignals(ctx) // OS signals listener
|
||||
@ -77,47 +185,72 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
oss.Stop() // stop system signals listening
|
||||
}()
|
||||
|
||||
// load templates content
|
||||
templates, loadingErr := cfg.LoadTemplates()
|
||||
if loadingErr != nil {
|
||||
return loadingErr
|
||||
} else if len(templates) == 0 {
|
||||
return errors.New("no loaded templates")
|
||||
}
|
||||
var (
|
||||
templateNames = cfg.TemplateNames()
|
||||
picker interface{ Pick() string }
|
||||
)
|
||||
|
||||
if f.template.name != "" && f.template.name != errorpage.UseRandom && f.template.name != errorpage.UseRandomOnEachRequest { //nolint:lll
|
||||
if _, found := templates[f.template.name]; !found {
|
||||
return errors.New("requested nonexistent template: " + f.template.name) // requested unknown template
|
||||
switch opt.Template.Name {
|
||||
case useRandomTemplate:
|
||||
log.Info("A random template will be used")
|
||||
|
||||
picker = pick.NewStringsSlice(templateNames, pick.RandomOnce)
|
||||
|
||||
case useRandomTemplateOnEachRequest:
|
||||
log.Info("A random template on EACH request will be used")
|
||||
|
||||
picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime)
|
||||
|
||||
case 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:
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// burn the error codes map
|
||||
codes := make(map[string]tpl.Annotator)
|
||||
for code, desc := range cfg.Pages {
|
||||
codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
|
||||
}
|
||||
|
||||
// create HTTP server
|
||||
server := appHttp.NewServer(log)
|
||||
|
||||
// register server routes, middlewares, etc.
|
||||
if err := server.Register(f.template.name, templates, codes); err != nil {
|
||||
if err := server.Register(cfg, picker, opt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startingErrCh := make(chan error, 1) // channel for server starting error
|
||||
startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
|
||||
|
||||
// start HTTP server in separate goroutine
|
||||
go func(errCh chan<- error) {
|
||||
defer close(errCh)
|
||||
|
||||
log.Info("Server starting",
|
||||
zap.String("addr", f.listen.ip),
|
||||
zap.Uint16("port", f.listen.port),
|
||||
zap.String("template name", f.template.name),
|
||||
zap.String("addr", ip),
|
||||
zap.Uint16("port", 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),
|
||||
zap.Bool("catch all enabled", opt.CatchAll),
|
||||
)
|
||||
|
||||
if err := server.Start(f.listen.ip, f.listen.port); err != nil {
|
||||
if err := server.Start(ip, port); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}(startingErrCh)
|
||||
@ -128,20 +261,18 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
|
||||
return err
|
||||
|
||||
case <-ctx.Done(): // ..or context cancellation
|
||||
log.Info("Gracefully server stopping")
|
||||
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
|
||||
|
||||
stoppedAt := time.Now()
|
||||
|
||||
// stop the server using created context above
|
||||
if err := server.Stop(serverShutdownTimeout); err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Error("Server stopping timeout exceeded", zap.Duration("timeout", serverShutdownTimeout))
|
||||
if p, ok := picker.(interface{ Close() error }); ok {
|
||||
if err := p.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Server stopped", zap.Duration("stopping duration", time.Since(stoppedAt)))
|
||||
// stop the server using created context above
|
||||
if err := server.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -1,91 +0,0 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
)
|
||||
|
||||
type flags struct {
|
||||
listen struct {
|
||||
ip string
|
||||
port uint16
|
||||
}
|
||||
template struct {
|
||||
name string
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
listenFlagName = "listen"
|
||||
portFlagName = "port"
|
||||
templateNameFlagName = "template-name"
|
||||
)
|
||||
|
||||
func (f *flags) init(flagSet *pflag.FlagSet) {
|
||||
flagSet.StringVarP(
|
||||
&f.listen.ip,
|
||||
listenFlagName, "l",
|
||||
"0.0.0.0",
|
||||
fmt.Sprintf("IP address to listen on [$%s]", env.ListenAddr),
|
||||
)
|
||||
flagSet.Uint16VarP(
|
||||
&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),
|
||||
)
|
||||
flagSet.StringVarP(
|
||||
&f.template.name,
|
||||
templateNameFlagName, "t",
|
||||
"",
|
||||
fmt.Sprintf(
|
||||
"template name (set \"%s\" to use the randomized or \"%s\" to use the randomized template on each request) [$%s]", //nolint:lll
|
||||
errorpage.UseRandom, errorpage.UseRandomOnEachRequest, env.TemplateName,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar)
|
||||
}
|
||||
}
|
||||
|
||||
case templateNameFlagName:
|
||||
if envVar, exists := env.TemplateName.Lookup(); exists {
|
||||
f.template.name = strings.TrimSpace(envVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
31
internal/cli/shared/flags.go
Normal file
31
internal/cli/shared/flags.go
Normal file
@ -0,0 +1,31 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/env"
|
||||
)
|
||||
|
||||
var ConfigFileFlag = &cli.StringFlag{ //nolint:gochecknoglobals
|
||||
Name: "config-file",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "path to the config file (yaml)",
|
||||
Value: "./error-pages.yml",
|
||||
EnvVars: []string{env.ConfigFilePath.String()},
|
||||
}
|
||||
|
||||
var ListenAddrFlag = &cli.StringFlag{ //nolint:gochecknoglobals
|
||||
Name: "listen",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "IP (v4 or v6) address to Listen on",
|
||||
Value: "0.0.0.0",
|
||||
EnvVars: []string{env.ListenAddr.String()},
|
||||
}
|
||||
|
||||
var ListenPortFlag = &cli.UintFlag{ //nolint:gochecknoglobals
|
||||
Name: "port",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "TCP port number",
|
||||
Value: 8080, //nolint:gomnd
|
||||
EnvVars: []string{env.ListenPort.String()},
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
// Package version contains CLI `version` command implementation.
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewCommand creates `version` command.
|
||||
func NewCommand(ver string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Aliases: []string{"v", "ver"},
|
||||
Short: "Display application version",
|
||||
RunE: func(*cobra.Command, []string) (err error) {
|
||||
_, err = fmt.Fprintf(os.Stdout, "app version:\t%s (%s)\n", ver, runtime.Version())
|
||||
|
||||
return
|
||||
},
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/cli/version"
|
||||
)
|
||||
|
||||
func TestProperties(t *testing.T) {
|
||||
cmd := version.NewCommand("")
|
||||
|
||||
assert.Equal(t, "version", cmd.Use)
|
||||
assert.ElementsMatch(t, []string{"v", "ver"}, cmd.Aliases)
|
||||
assert.NotNil(t, cmd.RunE)
|
||||
}
|
||||
|
||||
func TestCommandRun(t *testing.T) {
|
||||
cmd := version.NewCommand("1.2.3@foobar")
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureStdout(func() {
|
||||
assert.NoError(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "1.2.3@foobar")
|
||||
assert.Contains(t, output, runtime.Version())
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -11,20 +12,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 = os.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 +150,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)
|
||||
bytes, err := os.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,204 +1,43 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
|
||||
"gh.tarampamp.am/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 +50,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 +135,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:
|
||||
|
17
internal/env/env.go
vendored
17
internal/env/env.go
vendored
@ -6,10 +6,19 @@ import "os"
|
||||
type envVariable string
|
||||
|
||||
const (
|
||||
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
|
||||
ListenPort envVariable = "LISTEN_PORT" // port number for listening
|
||||
TemplateName envVariable = "TEMPLATE_NAME" // template name
|
||||
ConfigFilePath envVariable = "CONFIG_FILE" // path to the config file
|
||||
LogLevel envVariable = "LOG_LEVEL" // logging level
|
||||
LogFormat envVariable = "LOG_FORMAT" // logging format (json|console)
|
||||
|
||||
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
|
||||
ListenPort envVariable = "LISTEN_PORT" // port number for listening
|
||||
TemplateName envVariable = "TEMPLATE_NAME" // template name
|
||||
ConfigFilePath envVariable = "CONFIG_FILE" // path to the config file
|
||||
DefaultErrorPage envVariable = "DEFAULT_ERROR_PAGE" // default error page (code)
|
||||
DefaultHTTPCode envVariable = "DEFAULT_HTTP_CODE" // default HTTP response code
|
||||
ShowDetails envVariable = "SHOW_DETAILS" // show request details in response
|
||||
ProxyHTTPHeaders envVariable = "PROXY_HTTP_HEADERS" // proxy HTTP request headers list (request -> response)
|
||||
DisableL10n envVariable = "DISABLE_L10N" // disable pages localization
|
||||
CatchAll envVariable = "CATCH_ALL" // catch all pages
|
||||
)
|
||||
|
||||
// String returns environment variable name in the string representation.
|
||||
|
12
internal/env/env_test.go
vendored
12
internal/env/env_test.go
vendored
@ -12,6 +12,12 @@ func TestConstants(t *testing.T) {
|
||||
assert.Equal(t, "LISTEN_PORT", string(ListenPort))
|
||||
assert.Equal(t, "TEMPLATE_NAME", string(TemplateName))
|
||||
assert.Equal(t, "CONFIG_FILE", string(ConfigFilePath))
|
||||
assert.Equal(t, "DEFAULT_ERROR_PAGE", string(DefaultErrorPage))
|
||||
assert.Equal(t, "DEFAULT_HTTP_CODE", string(DefaultHTTPCode))
|
||||
assert.Equal(t, "SHOW_DETAILS", string(ShowDetails))
|
||||
assert.Equal(t, "PROXY_HTTP_HEADERS", string(ProxyHTTPHeaders))
|
||||
assert.Equal(t, "DISABLE_L10N", string(DisableL10n))
|
||||
assert.Equal(t, "CATCH_ALL", string(CatchAll))
|
||||
}
|
||||
|
||||
func TestEnvVariable_Lookup(t *testing.T) {
|
||||
@ -22,6 +28,12 @@ func TestEnvVariable_Lookup(t *testing.T) {
|
||||
{giveEnv: ListenPort},
|
||||
{giveEnv: TemplateName},
|
||||
{giveEnv: ConfigFilePath},
|
||||
{giveEnv: DefaultErrorPage},
|
||||
{giveEnv: DefaultHTTPCode},
|
||||
{giveEnv: ShowDetails},
|
||||
{giveEnv: ProxyHTTPHeaders},
|
||||
{giveEnv: DisableL10n},
|
||||
{giveEnv: CatchAll},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
|
@ -1,35 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
const internalErrorPattern = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>Internal error occurred</title>
|
||||
<style>
|
||||
html,body {background-color: #0e0e0e;color:#fff;font-family:'Nunito',sans-serif;height:100%;margin:0}
|
||||
.message {height:100%;align-items:center;display:flex;justify-content:center;position:relative;font-size:1.4em}
|
||||
img {padding-right: .4em}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message">
|
||||
<img src="https://hsto.org/webt/fs/sx/gt/fssxgtssfg689qxboqvjil5yz8g.png" alt="logo" height="32">
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
func HandleInternalHTTPError(ctx *fasthttp.RequestCtx, statusCode int, message string) {
|
||||
ctx.SetStatusCode(statusCode)
|
||||
ctx.SetContentType("text/html; charset=UTF-8")
|
||||
|
||||
_, _ = ctx.WriteString(strings.ReplaceAll(internalErrorPattern, "{{ message }}", message))
|
||||
}
|
@ -9,25 +9,60 @@ 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),
|
||||
zap.String("method", string(ctx.Method())),
|
||||
zap.String("url", string(ctx.RequestURI())),
|
||||
zap.String("referer", string(ctx.Referer())),
|
||||
zap.Int("status_code", ctx.Response.StatusCode()),
|
||||
zap.String("content_type", string(ctx.Response.Header.ContentType())),
|
||||
zap.Bool("connection_close", ctx.Response.ConnectionClose()),
|
||||
zap.Duration("duration", time.Since(startedAt)),
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
127
internal/http/core/errorpage.go
Normal file
127
internal/http/core/errorpage.go
Normal file
@ -0,0 +1,127 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/options"
|
||||
"gh.tarampamp.am/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
}
|
118
internal/http/core/formats_test.go
Normal file
118
internal/http/core/formats_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/http/core"
|
||||
)
|
||||
|
||||
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,85 +1,35 @@
|
||||
package errorpage
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tarampampam/error-pages/internal/http/common"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/http/core"
|
||||
"gh.tarampamp.am/error-pages/internal/options"
|
||||
"gh.tarampamp.am/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
const (
|
||||
UseRandom = "random"
|
||||
UseRandomOnEachRequest = "i-said-random"
|
||||
type (
|
||||
templatePicker interface {
|
||||
// Pick the template name for responding.
|
||||
Pick() string
|
||||
}
|
||||
|
||||
renderer interface {
|
||||
Render(content []byte, props tpl.Properties) ([]byte, error)
|
||||
}
|
||||
)
|
||||
|
||||
// NewHandler creates handler for error pages serving.
|
||||
func NewHandler(
|
||||
templateName string,
|
||||
templates map[string][]byte,
|
||||
codes map[string]tpl.Annotator,
|
||||
) (fasthttp.RequestHandler, error) {
|
||||
if len(templates) == 0 {
|
||||
return nil, errors.New("empty templates map")
|
||||
}
|
||||
|
||||
var (
|
||||
rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
|
||||
templateNames = templateTames(templates)
|
||||
)
|
||||
|
||||
if templateName == "" { // on empty template name
|
||||
templateName = templateNames[0] // pick the first
|
||||
} else if templateName == UseRandom { // on "random" template name
|
||||
templateName = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
|
||||
}
|
||||
|
||||
if _, found := templates[templateName]; !found && templateName != UseRandomOnEachRequest {
|
||||
return nil, errors.New("wrong template name passed")
|
||||
}
|
||||
|
||||
var pages = tpl.NewErrors(templates, codes)
|
||||
|
||||
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
var useTemplate = templateName // default
|
||||
|
||||
if templateName == UseRandomOnEachRequest {
|
||||
useTemplate = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
|
||||
}
|
||||
core.SetClientFormat(ctx, core.PlainTextContentType) // default content type
|
||||
|
||||
if code, ok := ctx.UserValue("code").(string); ok {
|
||||
if content, err := pages.Get(useTemplate, code); err == nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
_, _ = ctx.Write(content)
|
||||
} else {
|
||||
common.HandleInternalHTTPError(
|
||||
ctx,
|
||||
fasthttp.StatusNotFound,
|
||||
"requested code not available: "+err.Error(),
|
||||
)
|
||||
}
|
||||
} else { // will never happen
|
||||
common.HandleInternalHTTPError(
|
||||
ctx,
|
||||
fasthttp.StatusInternalServerError,
|
||||
"cannot extract requested code from the request",
|
||||
)
|
||||
core.RespondWithErrorPage(ctx, cfg, p, rdr, code, fasthttp.StatusOK, opt)
|
||||
} else { // will never occur
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
_, _ = ctx.WriteString("cannot extract requested code from the request")
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func templateTames(templates map[string][]byte) []string {
|
||||
var templateNames = make([]string, 0, len(templates))
|
||||
|
||||
for name := range templates {
|
||||
templateNames = append(templateNames, name)
|
||||
}
|
||||
|
||||
sort.Strings(templateNames)
|
||||
|
||||
return templateNames
|
||||
}
|
||||
|
@ -19,5 +19,6 @@ func NewHandler(checker checker) fasthttp.RequestHandler {
|
||||
}
|
||||
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
_, _ = ctx.WriteString("OK")
|
||||
}
|
||||
}
|
||||
|
50
internal/http/handlers/index/handler.go
Normal file
50
internal/http/handlers/index/handler.go
Normal file
@ -0,0 +1,50 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/http/core"
|
||||
"gh.tarampamp.am/error-pages/internal/options"
|
||||
"gh.tarampamp.am/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
type (
|
||||
templatePicker interface {
|
||||
// Pick the template name for responding.
|
||||
Pick() string
|
||||
}
|
||||
|
||||
renderer interface {
|
||||
Render(content []byte, props tpl.Properties) ([]byte, error)
|
||||
}
|
||||
)
|
||||
|
||||
// NewHandler creates handler for the index page serving.
|
||||
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
pageCode, httpCode := opt.Default.PageCode, int(opt.Default.HTTPCode)
|
||||
|
||||
if returnCode, ok := extractCodeToReturn(ctx); ok {
|
||||
pageCode, httpCode = strconv.Itoa(returnCode), returnCode
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package common_test
|
||||
package index_test
|
||||
|
||||
import "testing"
|
||||
|
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")
|
||||
}
|
28
internal/http/handlers/notfound/handler.go
Normal file
28
internal/http/handlers/notfound/handler.go
Normal file
@ -0,0 +1,28 @@
|
||||
package notfound
|
||||
|
||||
import (
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/http/core"
|
||||
"gh.tarampamp.am/error-pages/internal/options"
|
||||
"gh.tarampamp.am/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
type (
|
||||
templatePicker interface {
|
||||
// Pick the template name for responding.
|
||||
Pick() string
|
||||
}
|
||||
|
||||
renderer interface {
|
||||
Render(content []byte, props tpl.Properties) ([]byte, error)
|
||||
}
|
||||
)
|
||||
|
||||
// NewHandler creates handler missing requests handling.
|
||||
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
core.RespondWithErrorPage(ctx, cfg, p, rdr, "404", fasthttp.StatusNotFound, opt)
|
||||
}
|
||||
}
|
7
internal/http/handlers/notfound/handler_test.go
Normal file
7
internal/http/handlers/notfound/handler_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package notfound_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
@ -1,35 +1,46 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fasthttp/router"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
"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"
|
||||
versionHandler "github.com/tarampampam/error-pages/internal/http/handlers/version"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"github.com/tarampampam/error-pages/internal/version"
|
||||
"github.com/valyala/fasthttp"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/checkers"
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/http/common"
|
||||
errorpageHandler "gh.tarampamp.am/error-pages/internal/http/handlers/errorpage"
|
||||
healthzHandler "gh.tarampamp.am/error-pages/internal/http/handlers/healthz"
|
||||
indexHandler "gh.tarampamp.am/error-pages/internal/http/handlers/index"
|
||||
metricsHandler "gh.tarampamp.am/error-pages/internal/http/handlers/metrics"
|
||||
notfoundHandler "gh.tarampamp.am/error-pages/internal/http/handlers/notfound"
|
||||
versionHandler "gh.tarampamp.am/error-pages/internal/http/handlers/version"
|
||||
"gh.tarampamp.am/error-pages/internal/metrics"
|
||||
"gh.tarampamp.am/error-pages/internal/options"
|
||||
"gh.tarampamp.am/error-pages/internal/tpl"
|
||||
"gh.tarampamp.am/error-pages/internal/version"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
log *zap.Logger
|
||||
fast *fasthttp.Server
|
||||
router *router.Router
|
||||
rdr *tpl.TemplateRenderer
|
||||
}
|
||||
|
||||
const (
|
||||
defaultWriteTimeout = time.Second * 7
|
||||
defaultReadTimeout = time.Second * 7
|
||||
defaultIdleTimeout = time.Second * 15
|
||||
defaultWriteTimeout = time.Second * 4
|
||||
defaultReadTimeout = time.Second * 4
|
||||
defaultIdleTimeout = time.Second * 6
|
||||
)
|
||||
|
||||
func NewServer(log *zap.Logger) Server {
|
||||
r := router.New()
|
||||
rdr := tpl.NewTemplateRenderer()
|
||||
|
||||
return Server{
|
||||
// fasthttp docs: <https://github.com/valyala/fasthttp>
|
||||
@ -37,70 +48,83 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
// Start server.
|
||||
func (s *Server) Start(ip string, port uint16) error {
|
||||
return s.fast.ListenAndServe(ip + ":" + strconv.Itoa(int(port)))
|
||||
func (s *Server) Start(ip string, port uint16) (err error) {
|
||||
if net.ParseIP(ip) == nil {
|
||||
return errors.New("invalid IP address")
|
||||
}
|
||||
|
||||
var ln net.Listener
|
||||
|
||||
if strings.Count(ip, ":") >= 2 { //nolint:gomnd // ipv6
|
||||
if ln, err = net.Listen("tcp6", fmt.Sprintf("[%s]:%d", ip, port)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else { // ipv4
|
||||
if ln, err = net.Listen("tcp4", fmt.Sprintf("%s:%d", ip, port)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.fast.Serve(ln)
|
||||
}
|
||||
|
||||
type templatePicker interface {
|
||||
// Pick the template name for responding.
|
||||
Pick() string
|
||||
}
|
||||
|
||||
// Register server routes, middlewares, etc.
|
||||
// Router docs: <https://github.com/fasthttp/router>
|
||||
func (s *Server) Register(
|
||||
templateName string,
|
||||
templates map[string][]byte,
|
||||
codes map[string]tpl.Annotator,
|
||||
) error {
|
||||
s.router.GET("/", func(ctx *fasthttp.RequestCtx) {
|
||||
common.HandleInternalHTTPError(
|
||||
ctx,
|
||||
fasthttp.StatusNotFound,
|
||||
"Hi there! Error pages are available at the following URLs: /{code}.html",
|
||||
)
|
||||
})
|
||||
func (s *Server) Register(cfg *config.Config, templatePicker templatePicker, opt options.ErrorPage) error {
|
||||
reg, m := metrics.NewRegistry(), metrics.NewMetrics()
|
||||
|
||||
s.router.NotFound = func(ctx *fasthttp.RequestCtx) {
|
||||
common.HandleInternalHTTPError(
|
||||
ctx,
|
||||
fasthttp.StatusNotFound,
|
||||
"Wrong request URL. Error pages are available at the following URLs: /{code}.html",
|
||||
)
|
||||
if err := m.Register(reg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.router.GET("/version", versionHandler.NewHandler(version.Version()))
|
||||
s.router.ANY("/health/live", healthzHandler.NewHandler(checkers.NewLiveChecker()))
|
||||
s.fast.Handler = common.DurationMetrics(common.LogRequest(s.router.Handler, s.log), &m)
|
||||
|
||||
if h, err := errorpageHandler.NewHandler(templateName, templates, codes); err != nil {
|
||||
return err
|
||||
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()))
|
||||
|
||||
liveHandler := healthzHandler.NewHandler(checkers.NewLiveChecker())
|
||||
s.router.ANY("/healthz", liveHandler)
|
||||
s.router.ANY("/health/live", liveHandler) // deprecated
|
||||
|
||||
s.router.GET("/metrics", metricsHandler.NewHandler(reg))
|
||||
|
||||
// use index handler to catch all paths? Uses DEFAULT_ERROR_PAGE
|
||||
if opt.CatchAll {
|
||||
s.router.NotFound = indexHandler.NewHandler(cfg, templatePicker, s.rdr, opt)
|
||||
} else {
|
||||
s.router.GET("/{code}.html", h)
|
||||
// use default not found handler
|
||||
s.router.NotFound = notfoundHandler.NewHandler(cfg, templatePicker, s.rdr, opt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop server.
|
||||
func (s *Server) Stop(timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout) // TODO replace with simple time.After
|
||||
defer cancel()
|
||||
func (s *Server) Stop() error {
|
||||
if err := s.rdr.Close(); err != nil {
|
||||
defer func() { _ = s.fast.Shutdown() }()
|
||||
|
||||
ch := make(chan error, 1) // channel for server stopping error
|
||||
|
||||
go func() { defer close(ch); ch <- s.fast.Shutdown() }()
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
return s.fast.Shutdown()
|
||||
}
|
||||
|
68
internal/logger/format.go
Normal file
68
internal/logger/format.go
Normal file
@ -0,0 +1,68 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Format is a logging format.
|
||||
type Format uint8
|
||||
|
||||
const (
|
||||
ConsoleFormat Format = iota // useful for console output (for humans)
|
||||
JSONFormat // useful for logging aggregation systems (for robots)
|
||||
)
|
||||
|
||||
// String returns a lower-case ASCII representation of the log format.
|
||||
func (f Format) String() string {
|
||||
switch f {
|
||||
case ConsoleFormat:
|
||||
return "console"
|
||||
case JSONFormat:
|
||||
return "json"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("format(%d)", f)
|
||||
}
|
||||
|
||||
// Formats returns a slice of all logging formats.
|
||||
func Formats() []Format {
|
||||
return []Format{ConsoleFormat, JSONFormat}
|
||||
}
|
||||
|
||||
// FormatStrings returns a slice of all logging formats as strings.
|
||||
func FormatStrings() []string {
|
||||
var (
|
||||
formats = Formats()
|
||||
result = make([]string, len(formats))
|
||||
)
|
||||
|
||||
for i := range formats {
|
||||
result[i] = formats[i].String()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseFormat parses a format (case is ignored) based on the ASCII representation of the log format.
|
||||
// If the provided ASCII representation is invalid an error is returned.
|
||||
//
|
||||
// This is particularly useful when dealing with text input to configure log formats.
|
||||
func ParseFormat[T string | []byte](text T) (Format, error) {
|
||||
var format string
|
||||
|
||||
if s, ok := any(text).(string); ok {
|
||||
format = s
|
||||
} else {
|
||||
format = string(any(text).([]byte))
|
||||
}
|
||||
|
||||
switch strings.ToLower(format) {
|
||||
case "console", "": // make the zero value useful
|
||||
return ConsoleFormat, nil
|
||||
case "json":
|
||||
return JSONFormat, nil
|
||||
}
|
||||
|
||||
return Format(0), fmt.Errorf("unrecognized logging format: %q", text)
|
||||
}
|
62
internal/logger/format_test.go
Normal file
62
internal/logger/format_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package logger_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/logger"
|
||||
)
|
||||
|
||||
func TestFormat_String(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveFormat logger.Format
|
||||
wantString string
|
||||
}{
|
||||
"json": {giveFormat: logger.JSONFormat, wantString: "json"},
|
||||
"console": {giveFormat: logger.ConsoleFormat, wantString: "console"},
|
||||
"<unknown>": {giveFormat: logger.Format(255), wantString: "format(255)"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require.Equal(t, tt.wantString, tt.giveFormat.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFormat(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveBytes []byte
|
||||
giveString string
|
||||
wantFormat logger.Format
|
||||
wantError error
|
||||
}{
|
||||
"<empty value>": {giveBytes: []byte(""), wantFormat: logger.ConsoleFormat},
|
||||
"<empty value> (string)": {giveString: "", wantFormat: logger.ConsoleFormat},
|
||||
"console": {giveBytes: []byte("console"), wantFormat: logger.ConsoleFormat},
|
||||
"console (string)": {giveString: "console", wantFormat: logger.ConsoleFormat},
|
||||
"json": {giveBytes: []byte("json"), wantFormat: logger.JSONFormat},
|
||||
"json (string)": {giveString: "json", wantFormat: logger.JSONFormat},
|
||||
"foobar": {giveBytes: []byte("foobar"), wantError: errors.New("unrecognized logging format: \"foobar\"")}, //nolint:lll
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var (
|
||||
f logger.Format
|
||||
err error
|
||||
)
|
||||
|
||||
if tt.giveString != "" {
|
||||
f, err = logger.ParseFormat(tt.giveString)
|
||||
} else {
|
||||
f, err = logger.ParseFormat(tt.giveBytes)
|
||||
}
|
||||
|
||||
if tt.wantError == nil {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantFormat, f)
|
||||
} else {
|
||||
require.EqualError(t, err, tt.wantError.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
83
internal/logger/level.go
Normal file
83
internal/logger/level.go
Normal file
@ -0,0 +1,83 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Level is a logging level.
|
||||
type Level int8
|
||||
|
||||
const (
|
||||
DebugLevel Level = iota - 1
|
||||
InfoLevel // default level (zero-value)
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
FatalLevel
|
||||
)
|
||||
|
||||
// String returns a lower-case ASCII representation of the log level.
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case DebugLevel:
|
||||
return "debug"
|
||||
case InfoLevel:
|
||||
return "info"
|
||||
case WarnLevel:
|
||||
return "warn"
|
||||
case ErrorLevel:
|
||||
return "error"
|
||||
case FatalLevel:
|
||||
return "fatal"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("level(%d)", l)
|
||||
}
|
||||
|
||||
// Levels returns a slice of all logging levels.
|
||||
func Levels() []Level {
|
||||
return []Level{DebugLevel, InfoLevel, WarnLevel, ErrorLevel, FatalLevel}
|
||||
}
|
||||
|
||||
// LevelStrings returns a slice of all logging levels as strings.
|
||||
func LevelStrings() []string {
|
||||
var (
|
||||
levels = Levels()
|
||||
result = make([]string, len(levels))
|
||||
)
|
||||
|
||||
for i := range levels {
|
||||
result[i] = levels[i].String()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseLevel parses a level (case is ignored) based on the ASCII representation of the log level.
|
||||
// If the provided ASCII representation is invalid an error is returned.
|
||||
//
|
||||
// This is particularly useful when dealing with text input to configure log levels.
|
||||
func ParseLevel[T string | []byte](text T) (Level, error) {
|
||||
var lvl string
|
||||
|
||||
if s, ok := any(text).(string); ok {
|
||||
lvl = s
|
||||
} else {
|
||||
lvl = string(any(text).([]byte))
|
||||
}
|
||||
|
||||
switch strings.ToLower(lvl) {
|
||||
case "debug", "verbose", "trace":
|
||||
return DebugLevel, nil
|
||||
case "info", "": // make the zero value useful
|
||||
return InfoLevel, nil
|
||||
case "warn":
|
||||
return WarnLevel, nil
|
||||
case "error":
|
||||
return ErrorLevel, nil
|
||||
case "fatal":
|
||||
return FatalLevel, nil
|
||||
}
|
||||
|
||||
return Level(0), fmt.Errorf("unrecognized logging level: %q", text)
|
||||
}
|
84
internal/logger/level_test.go
Normal file
84
internal/logger/level_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package logger_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/logger"
|
||||
)
|
||||
|
||||
func TestLevel_String(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveLevel logger.Level
|
||||
wantString string
|
||||
}{
|
||||
"debug": {giveLevel: logger.DebugLevel, wantString: "debug"},
|
||||
"info": {giveLevel: logger.InfoLevel, wantString: "info"},
|
||||
"warn": {giveLevel: logger.WarnLevel, wantString: "warn"},
|
||||
"error": {giveLevel: logger.ErrorLevel, wantString: "error"},
|
||||
"fatal": {giveLevel: logger.FatalLevel, wantString: "fatal"},
|
||||
"<unknown>": {giveLevel: logger.Level(127), wantString: "level(127)"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
require.Equal(t, tt.wantString, tt.giveLevel.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLevel(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveBytes []byte
|
||||
giveString string
|
||||
wantLevel logger.Level
|
||||
wantError error
|
||||
}{
|
||||
"<empty value>": {giveBytes: []byte(""), wantLevel: logger.InfoLevel},
|
||||
"<empty value> (string)": {giveString: "", wantLevel: logger.InfoLevel},
|
||||
"trace": {giveBytes: []byte("debug"), wantLevel: logger.DebugLevel},
|
||||
"verbose": {giveBytes: []byte("debug"), wantLevel: logger.DebugLevel},
|
||||
"debug": {giveBytes: []byte("debug"), wantLevel: logger.DebugLevel},
|
||||
"debug (string)": {giveString: "debug", wantLevel: logger.DebugLevel},
|
||||
"info": {giveBytes: []byte("info"), wantLevel: logger.InfoLevel},
|
||||
"warn": {giveBytes: []byte("warn"), wantLevel: logger.WarnLevel},
|
||||
"error": {giveBytes: []byte("error"), wantLevel: logger.ErrorLevel},
|
||||
"fatal": {giveBytes: []byte("fatal"), wantLevel: logger.FatalLevel},
|
||||
"fatal (string)": {giveString: "fatal", wantLevel: logger.FatalLevel},
|
||||
"foobar": {giveBytes: []byte("foobar"), wantError: errors.New("unrecognized logging level: \"foobar\"")}, //nolint:lll
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var (
|
||||
l logger.Level
|
||||
err error
|
||||
)
|
||||
|
||||
if tt.giveString != "" {
|
||||
l, err = logger.ParseLevel(tt.giveString)
|
||||
} else {
|
||||
l, err = logger.ParseLevel(tt.giveBytes)
|
||||
}
|
||||
|
||||
if tt.wantError == nil {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantLevel, l)
|
||||
} else {
|
||||
require.EqualError(t, err, tt.wantError.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevels(t *testing.T) {
|
||||
require.Equal(t, []logger.Level{
|
||||
logger.DebugLevel,
|
||||
logger.InfoLevel,
|
||||
logger.WarnLevel,
|
||||
logger.ErrorLevel,
|
||||
logger.FatalLevel,
|
||||
}, logger.Levels())
|
||||
}
|
||||
|
||||
func TestLevelStrings(t *testing.T) {
|
||||
require.Equal(t, []string{"debug", "info", "warn", "error", "fatal"}, logger.LevelStrings())
|
||||
}
|
@ -2,20 +2,27 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// New creates new "zap" logger with little customization.
|
||||
func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
|
||||
// New creates new "zap" logger with a small customization.
|
||||
func New(l Level, f Format) (*zap.Logger, error) {
|
||||
var config zap.Config
|
||||
|
||||
if logJSON {
|
||||
config = zap.NewProductionConfig()
|
||||
} else {
|
||||
switch f {
|
||||
case ConsoleFormat:
|
||||
config = zap.NewDevelopmentConfig()
|
||||
config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
|
||||
config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05")
|
||||
|
||||
case JSONFormat:
|
||||
config = zap.NewProductionConfig() // json encoder is used by default
|
||||
|
||||
default:
|
||||
return nil, errors.New("unsupported logging format")
|
||||
}
|
||||
|
||||
// default configuration for all encoders
|
||||
@ -24,15 +31,31 @@ func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
|
||||
config.DisableStacktrace = true
|
||||
config.DisableCaller = true
|
||||
|
||||
if debug {
|
||||
// enable additional features for debugging
|
||||
if l <= DebugLevel {
|
||||
config.Development = true
|
||||
config.DisableStacktrace = false
|
||||
config.DisableCaller = false
|
||||
}
|
||||
|
||||
if verbose || debug {
|
||||
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
|
||||
var zapLvl zapcore.Level
|
||||
|
||||
switch l { // convert level to zap.Level
|
||||
case DebugLevel:
|
||||
zapLvl = zap.DebugLevel
|
||||
case InfoLevel:
|
||||
zapLvl = zap.InfoLevel
|
||||
case WarnLevel:
|
||||
zapLvl = zap.WarnLevel
|
||||
case ErrorLevel:
|
||||
zapLvl = zap.ErrorLevel
|
||||
case FatalLevel:
|
||||
zapLvl = zap.FatalLevel
|
||||
default:
|
||||
return nil, errors.New("unsupported logging level")
|
||||
}
|
||||
|
||||
config.Level = zap.NewAtomicLevelAt(zapLvl)
|
||||
|
||||
return config.Build()
|
||||
}
|
||||
|
@ -8,64 +8,51 @@ import (
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/logger"
|
||||
)
|
||||
|
||||
func TestNewNotVerboseDebugJSON(t *testing.T) {
|
||||
func TestNewDebugLevelConsoleFormat(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(false, false, false)
|
||||
assert.NoError(t, err)
|
||||
log, err := logger.New(logger.DebugLevel, logger.ConsoleFormat)
|
||||
require.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Info("inf msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
|
||||
assert.NotContains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewVerboseNotDebugJSON(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(true, false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
|
||||
assert.Contains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewVerboseDebugNotJSON(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(true, true, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\t.+logger_test\.go:\d+\tinf msg`, output)
|
||||
assert.Contains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewNotVerboseDebugButJSON(t *testing.T) {
|
||||
func TestNewErrorLevelConsoleFormat(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(false, false, true)
|
||||
assert.NoError(t, err)
|
||||
log, err := logger.New(logger.ErrorLevel, logger.ConsoleFormat)
|
||||
require.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Info("inf msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.NotContains(t, output, "inf msg")
|
||||
assert.NotContains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewWarnLevelJSONFormat(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(logger.WarnLevel, logger.JSONFormat)
|
||||
require.NoError(t, err)
|
||||
|
||||
log.Debug("dbg msg")
|
||||
log.Info("inf msg")
|
||||
log.Warn("warn msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
@ -75,6 +62,14 @@ func TestNewNotVerboseDebugButJSON(t *testing.T) {
|
||||
|
||||
lines := strings.Split(strings.Trim(output, "\n"), "\n")
|
||||
|
||||
assert.JSONEq(t, `{"level":"info","ts":0.1,"msg":"inf msg"}`, lines[0])
|
||||
assert.JSONEq(t, `{"level":"warn","ts":0.1,"msg":"warn msg"}`, lines[0])
|
||||
assert.JSONEq(t, `{"level":"error","ts":0.1,"msg":"err msg"}`, lines[1])
|
||||
}
|
||||
|
||||
func TestNewErrors(t *testing.T) {
|
||||
_, err := logger.New(logger.Level(127), logger.ConsoleFormat)
|
||||
require.EqualError(t, err, "unsupported logging level")
|
||||
|
||||
_, err = logger.New(logger.WarnLevel, logger.Format(255))
|
||||
require.EqualError(t, err, "unsupported logging format")
|
||||
}
|
||||
|
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
|
||||
}
|
71
internal/metrics/metrics_test.go
Normal file
71
internal/metrics/metrics_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
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"
|
||||
|
||||
"gh.tarampamp.am/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
|
||||
}
|
19
internal/metrics/registry_test.go
Normal file
19
internal/metrics/registry_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package metrics_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/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")
|
||||
}
|
17
internal/options/errorpage.go
Normal file
17
internal/options/errorpage.go
Normal file
@ -0,0 +1,17 @@
|
||||
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
|
||||
CatchAll bool // catch all pages
|
||||
}
|
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
|
||||
}
|
58
internal/pick/picker_test.go
Normal file
58
internal/pick/picker_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
package pick_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/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() })
|
||||
}
|
135
internal/pick/strings_slice.go
Normal file
135
internal/pick/strings_slice.go
Normal file
@ -0,0 +1,135 @@
|
||||
package pick
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StringsSlice struct {
|
||||
s []string
|
||||
p *picker
|
||||
}
|
||||
|
||||
// NewStringsSlice creates new StringsSlice.
|
||||
func NewStringsSlice(items []string, mode pickMode) *StringsSlice {
|
||||
maxIdx := len(items) - 1
|
||||
|
||||
if maxIdx < 0 {
|
||||
maxIdx = 0
|
||||
}
|
||||
|
||||
return &StringsSlice{s: items, p: NewPicker(uint32(maxIdx), mode)}
|
||||
}
|
||||
|
||||
// Pick an element from the strings slice.
|
||||
func (s *StringsSlice) Pick() string {
|
||||
if len(s.s) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return s.s[s.p.NextIndex()]
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
131
internal/pick/strings_slice_test.go
Normal file
131
internal/pick/strings_slice_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
package pick_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/pick"
|
||||
)
|
||||
|
||||
func TestStringsSlice_Pick(t *testing.T) {
|
||||
t.Run("first", func(t *testing.T) {
|
||||
for i := uint8(0); i < 100; i++ {
|
||||
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.First).Pick())
|
||||
}
|
||||
|
||||
p := pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.First)
|
||||
|
||||
for i := uint8(0); i < 100; i++ {
|
||||
assert.Equal(t, "foo", p.Pick())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("random once", func(t *testing.T) {
|
||||
for i := uint8(0); i < 100; i++ {
|
||||
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.RandomOnce).Pick())
|
||||
}
|
||||
|
||||
var (
|
||||
p = pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.RandomOnce)
|
||||
picked = p.Pick()
|
||||
)
|
||||
|
||||
for i := uint8(0); i < 100; i++ {
|
||||
assert.Equal(t, picked, p.Pick())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("random every time", func(t *testing.T) {
|
||||
for i := uint8(0); i < 100; i++ {
|
||||
assert.Equal(t, "", pick.NewStringsSlice([]string{}, pick.RandomEveryTime).Pick())
|
||||
}
|
||||
|
||||
for i := uint8(0); i < 100; i++ {
|
||||
p := pick.NewStringsSlice([]string{"foo", "bar", "baz"}, pick.RandomEveryTime)
|
||||
|
||||
assert.NotEqual(t, p.Pick(), p.Pick())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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,102 +0,0 @@
|
||||
package tpl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Annotator allows to annotate error code.
|
||||
type Annotator struct {
|
||||
Message string
|
||||
Description string
|
||||
}
|
||||
|
||||
// Errors is a "cached storage" for the rendered error pages for the different templates and codes.
|
||||
type Errors struct {
|
||||
templates map[string][]byte
|
||||
codes map[string]Annotator
|
||||
|
||||
cacheMu sync.RWMutex
|
||||
cache map[string]map[string][]byte // map[template]map[code]content
|
||||
}
|
||||
|
||||
// NewErrors creates new Errors.
|
||||
func NewErrors(templates map[string][]byte, codes map[string]Annotator) *Errors {
|
||||
return &Errors{
|
||||
templates: templates,
|
||||
codes: codes,
|
||||
cache: make(map[string]map[string][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Errors) existsInCache(template, code string) ([]byte, bool) {
|
||||
e.cacheMu.RLock()
|
||||
defer e.cacheMu.RUnlock()
|
||||
|
||||
if codes, tplOk := e.cache[template]; tplOk {
|
||||
if content, codeOk := codes[code]; codeOk {
|
||||
return content, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (e *Errors) putInCache(template, code string) error {
|
||||
if _, ok := e.templates[template]; !ok {
|
||||
return errors.New("template \"" + template + "\" does not exists")
|
||||
}
|
||||
|
||||
if _, ok := e.codes[code]; !ok {
|
||||
return errors.New("code \"" + code + "\" does not exists")
|
||||
}
|
||||
|
||||
e.cacheMu.Lock()
|
||||
defer e.cacheMu.Unlock()
|
||||
|
||||
if _, ok := e.cache[template]; !ok {
|
||||
e.cache[template] = make(map[string][]byte)
|
||||
}
|
||||
|
||||
e.cache[template][code] = Replace(e.templates[template], Replaces{
|
||||
Code: code,
|
||||
Message: e.codes[code].Message,
|
||||
Description: e.codes[code].Description,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the rendered error page content.
|
||||
func (e *Errors) Get(template, code string) ([]byte, error) {
|
||||
if content, ok := e.existsInCache(template, code); ok {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
if err := e.putInCache(template, code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.cacheMu.RLock()
|
||||
defer e.cacheMu.RUnlock()
|
||||
|
||||
return e.cache[template][code], nil
|
||||
}
|
||||
|
||||
// VisitAll allows to iterate all possible error pages and templates.
|
||||
func (e *Errors) VisitAll(fn func(template, code string, content []byte) error) error {
|
||||
for tpl := range e.templates {
|
||||
for code := range e.codes {
|
||||
content, err := e.Get(tpl, code)
|
||||
if err != nil {
|
||||
return err // will never happen
|
||||
}
|
||||
|
||||
if err = fn(tpl, code, content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
package tpl_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
func TestErrors_Get(t *testing.T) {
|
||||
e := tpl.NewErrors(
|
||||
map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
|
||||
map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
|
||||
)
|
||||
|
||||
content, err := e.Get("foo", "200")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "200: ok all is ok", string(content))
|
||||
|
||||
content, err = e.Get("foo", "666")
|
||||
assert.EqualError(t, err, "code \"666\" does not exists")
|
||||
assert.Nil(t, content)
|
||||
|
||||
content, err = e.Get("bar", "200")
|
||||
assert.EqualError(t, err, "template \"bar\" does not exists")
|
||||
assert.Nil(t, content)
|
||||
}
|
||||
|
||||
func TestErrors_GetConcurrent(t *testing.T) {
|
||||
e := tpl.NewErrors(
|
||||
map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
|
||||
map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 1234; i++ {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
content, err := e.Get("foo", "200")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "200: ok all is ok", string(content))
|
||||
|
||||
content, err = e.Get("foo", "666")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, content)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestErrors_VisitAll(t *testing.T) {
|
||||
e := tpl.NewErrors(
|
||||
map[string][]byte{
|
||||
"foo": []byte("{{ code }}: {{ message }} {{ description }}"),
|
||||
"bar": []byte("{{ code }}: {{ message }} {{ description }}"),
|
||||
},
|
||||
map[string]tpl.Annotator{
|
||||
"200": {"ok", "all is ok"},
|
||||
"400": {"Bad Request", "The server did not understand the request"},
|
||||
},
|
||||
)
|
||||
|
||||
visited := make(map[string]map[string]bool) // map[template]codes
|
||||
|
||||
assert.NoError(t, e.VisitAll(func(template, code string, content []byte) error {
|
||||
if _, ok := visited[template]; !ok {
|
||||
visited[template] = make(map[string]bool)
|
||||
}
|
||||
|
||||
visited[template][code] = true
|
||||
|
||||
assert.NotNil(t, content)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
assert.Len(t, visited, 2)
|
||||
assert.Len(t, visited["foo"], 2)
|
||||
assert.True(t, visited["foo"]["200"])
|
||||
assert.True(t, visited["foo"]["400"])
|
||||
assert.Len(t, visited["bar"], 2)
|
||||
assert.True(t, visited["bar"]["200"])
|
||||
assert.True(t, visited["bar"]["400"])
|
||||
}
|
||||
|
||||
func TestErrors_VisitAllWillReturnTheError(t *testing.T) {
|
||||
e := tpl.NewErrors(
|
||||
map[string][]byte{
|
||||
"foo": []byte("{{ code }}: {{ message }} {{ description }}"),
|
||||
},
|
||||
map[string]tpl.Annotator{
|
||||
"200": {"ok", "all is ok"},
|
||||
},
|
||||
)
|
||||
|
||||
assert.EqualError(t, e.VisitAll(func(template, code string, content []byte) error {
|
||||
return errors.New("foo error")
|
||||
}), "foo error")
|
||||
}
|
25
internal/tpl/hasher.go
Normal file
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
|
||||
}
|
36
internal/tpl/hasher_test.go
Normal file
36
internal/tpl/hasher_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package tpl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/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) }
|
67
internal/tpl/properties_test.go
Normal file
67
internal/tpl/properties_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package tpl_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"gh.tarampamp.am/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)
|
||||
}
|
220
internal/tpl/render.go
Normal file
220
internal/tpl/render.go
Normal file
@ -0,0 +1,220 @@
|
||||
package tpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"gh.tarampamp.am/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
|
||||
},
|
||||
"env": os.Getenv,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
142
internal/tpl/render_test.go
Normal file
142
internal/tpl/render_test.go
Normal file
@ -0,0 +1,142 @@
|
||||
package tpl_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
func Test_Render(t *testing.T) {
|
||||
renderer := tpl.NewTemplateRenderer()
|
||||
defer func() { _ = renderer.Close() }()
|
||||
|
||||
require.NoError(t, os.Setenv("TEST_ENV_VAR", "unit-test"))
|
||||
|
||||
defer func() { require.NoError(t, os.Unsetenv("TEST_ENV_VAR")) }()
|
||||
|
||||
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",
|
||||
},
|
||||
|
||||
"env": {
|
||||
giveContent: `{{ env "TEST_ENV_VAR" }}`,
|
||||
wantContent: "unit-test",
|
||||
},
|
||||
} {
|
||||
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"},
|
||||
)
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package tpl
|
||||
|
||||
import "bytes"
|
||||
|
||||
type Replaces struct {
|
||||
Code string
|
||||
Message string
|
||||
Description string
|
||||
}
|
||||
|
||||
const (
|
||||
tknCode byte = iota + 1
|
||||
tknMessage
|
||||
tknDescription
|
||||
)
|
||||
|
||||
var tknSets = map[byte][][]byte{ //nolint:gochecknoglobals
|
||||
tknCode: {[]byte("{{code}}"), []byte("{{ code }}")},
|
||||
tknMessage: {[]byte("{{message}}"), []byte("{{ message }}")},
|
||||
tknDescription: {[]byte("{{description}}"), []byte("{{ description }}")},
|
||||
}
|
||||
|
||||
// Replace found tokens in the incoming slice with passed tokens.
|
||||
func Replace(in []byte, re Replaces) []byte {
|
||||
for tkn, set := range tknSets {
|
||||
var replaceWith []byte
|
||||
|
||||
switch tkn {
|
||||
case tknCode:
|
||||
replaceWith = []byte(re.Code)
|
||||
case tknMessage:
|
||||
replaceWith = []byte(re.Message)
|
||||
case tknDescription:
|
||||
replaceWith = []byte(re.Description)
|
||||
default:
|
||||
panic("tpl: unsupported token")
|
||||
}
|
||||
|
||||
if len(replaceWith) > 0 {
|
||||
for i := 0; i < len(set); i++ {
|
||||
in = bytes.ReplaceAll(in, set[i], replaceWith)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return in
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package tpl_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
func ExampleReplace() {
|
||||
var in = []byte("{{ code }}: {{message}} ({{ description }})")
|
||||
|
||||
fmt.Println(string(tpl.Replace(in, tpl.Replaces{
|
||||
Code: "400",
|
||||
Message: "Bad Request",
|
||||
Description: "The server did not understand the request",
|
||||
})))
|
||||
|
||||
// Output:
|
||||
// 400: Bad Request (The server did not understand the request)
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveIn []byte
|
||||
giveRe tpl.Replaces
|
||||
wantResult []byte
|
||||
}{
|
||||
"common": {
|
||||
giveIn: []byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "),
|
||||
giveRe: tpl.Replaces{
|
||||
Code: "123",
|
||||
Message: "message",
|
||||
Description: "desc",
|
||||
},
|
||||
wantResult: []byte("-- 123 123 __ message desc "),
|
||||
},
|
||||
"alpha and underline in the code": {
|
||||
giveIn: []byte("\t{{ code }}\t"),
|
||||
giveRe: tpl.Replaces{
|
||||
Code: " qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ ",
|
||||
},
|
||||
wantResult: []byte("\t qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ \t"),
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.wantResult, tpl.Replace(tt.giveIn, tt.giveRe))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReplace(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.Replace([]byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "), tpl.Replaces{
|
||||
Code: "123",
|
||||
Message: "message",
|
||||
Description: "desc",
|
||||
})
|
||||
}
|
||||
}
|
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
|
||||
}
|
||||
}
|
870
l10n/l10n.js
Normal file
870
l10n/l10n.js
Normal file
@ -0,0 +1,870 @@
|
||||
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',
|
||||
zh: '错误',
|
||||
id: 'Kesalahan',
|
||||
},
|
||||
'Good luck': {
|
||||
fr: 'Bonne chance',
|
||||
ru: 'Удачи',
|
||||
uk: 'Успіхів',
|
||||
pt: 'Boa sorte',
|
||||
nl: 'Veel succes',
|
||||
de: 'Viel Glück',
|
||||
es: 'Buena Suerte',
|
||||
zh: '祝好运',
|
||||
id: 'Semoga berhasil!',
|
||||
},
|
||||
'UH OH': {
|
||||
fr: 'Oups',
|
||||
ru: 'Ох',
|
||||
uk: 'Упс',
|
||||
pt: 'Ops',
|
||||
nl: 'Oeps',
|
||||
de: 'Hoppla',
|
||||
es: 'Oups',
|
||||
zh: '哎呀',
|
||||
id: 'Ups',
|
||||
},
|
||||
'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',
|
||||
zh: '请求详情',
|
||||
id: 'Rincian permintaan',
|
||||
},
|
||||
'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',
|
||||
zh: '请再次检查地址',
|
||||
id: 'Periksa 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',
|
||||
zh: '或返回上一页',
|
||||
id: 'Atau, kembali',
|
||||
},
|
||||
'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',
|
||||
zh: '可能原因有',
|
||||
id: 'Inilah yang bisa saja terjadi',
|
||||
},
|
||||
'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',
|
||||
zh: '您可能输入了错误的地址',
|
||||
id: 'Anda mungkin tersalah memasukkan 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',
|
||||
zh: '站点已被转移',
|
||||
id: 'Halaman dipindahkan',
|
||||
},
|
||||
'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í',
|
||||
zh: '站点从未存在',
|
||||
id: 'Itu Tidak pernah di sini',
|
||||
},
|
||||
'Bad Request': {
|
||||
fr: 'Mauvaise requête',
|
||||
ru: 'Некорректный запрос',
|
||||
uk: 'Хибний запит',
|
||||
pt: 'Requisição inválida',
|
||||
nl: 'Foutieve anvraag',
|
||||
de: 'Fehlerhafte Anfrage',
|
||||
es: 'Petición inválida',
|
||||
zh: '错误请求',
|
||||
id: 'Permintaan yang salah',
|
||||
},
|
||||
'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',
|
||||
zh: '服务器不理解该请求',
|
||||
id: 'Server tidak memahami permintaan',
|
||||
},
|
||||
'Unauthorized': {
|
||||
fr: 'Non autorisé',
|
||||
ru: 'Запрос не авторизован',
|
||||
uk: 'Несанкціонований доступ',
|
||||
pt: 'Não autorizado',
|
||||
nl: 'Niet geautoriseerd',
|
||||
de: 'Nicht autorisiert',
|
||||
es: 'No autorizado',
|
||||
zh: '未经授权',
|
||||
id: 'Tidak diotorisasi',
|
||||
},
|
||||
'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',
|
||||
zh: '请求的页面需要用户名和密码',
|
||||
id: 'Halaman yang diminta membutuhkan nama pengguna dan kata sandi',
|
||||
},
|
||||
'Forbidden': {
|
||||
fr: 'Interdit',
|
||||
ru: 'Запрещено',
|
||||
uk: 'Заборонено',
|
||||
pt: 'Proibido',
|
||||
nl: 'Verboden',
|
||||
de: 'Verboten',
|
||||
es: 'Prohibido',
|
||||
zh: '禁止访问',
|
||||
id: 'Dilarang',
|
||||
},
|
||||
'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',
|
||||
zh: '禁止访问请求的页面',
|
||||
id: 'Akses dilarang ke halaman yang diminta',
|
||||
},
|
||||
'Not Found': {
|
||||
fr: 'Introuvable',
|
||||
ru: 'Страница не найдена',
|
||||
uk: 'Сторінку не знайдено',
|
||||
pt: 'Não encontrado',
|
||||
nl: 'Niet gevonden',
|
||||
de: 'Nicht gefunden',
|
||||
es: 'No encontrado',
|
||||
zh: '未找到',
|
||||
id: 'Tidak ditemukan',
|
||||
},
|
||||
'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',
|
||||
zh: '服务器找不到请求的页面',
|
||||
id: 'Server tidak dapat menemukan halaman yang diminta',
|
||||
},
|
||||
'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',
|
||||
zh: '方法不被允许',
|
||||
id: 'Metode tidak diizinkan',
|
||||
},
|
||||
'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',
|
||||
zh: '请求指定的方法不被允许',
|
||||
id: 'Metode dalam permintaan tidak diizinkan',
|
||||
},
|
||||
'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',
|
||||
zh: '需要代理服务器身份验证',
|
||||
id: 'Diperlukan otentikasi proxy',
|
||||
},
|
||||
'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',
|
||||
zh: '您必须对代理服务器进行身份验证,然后才能让请求得到处理',
|
||||
id: 'Anda harus mengautentikasi dengan server proxy sebelum permintaan ini dapat dilayani',
|
||||
},
|
||||
'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',
|
||||
zh: '请求超时',
|
||||
id: 'Meminta batas waktu',
|
||||
},
|
||||
'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',
|
||||
zh: '请求用时超过了服务器设置的最长等待时间',
|
||||
id: 'Permintaan memakan waktu lebih lama dari yang bisa ditunggu oleh server',
|
||||
},
|
||||
'Conflict': {
|
||||
fr: 'Conflit',
|
||||
ru: 'Конфликт',
|
||||
uk: 'Конфлікт',
|
||||
pt: 'Conflito',
|
||||
nl: 'Conflict',
|
||||
de: 'Konflikt',
|
||||
es: 'Conflicto',
|
||||
zh: '冲突',
|
||||
id: 'Konflik',
|
||||
},
|
||||
'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',
|
||||
zh: '由于冲突,请求无法完成',
|
||||
id: 'Permintaan tidak dapat diselesaikan karena adanya konflik',
|
||||
},
|
||||
'Gone': {
|
||||
fr: 'Supprimé',
|
||||
ru: 'Удалено',
|
||||
uk: 'Вилучений',
|
||||
pt: 'Removido',
|
||||
nl: 'Verdwenen',
|
||||
de: 'Verschwunden',
|
||||
es: 'Eliminado',
|
||||
zh: '已移除',
|
||||
id: 'Menghilang',
|
||||
},
|
||||
'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',
|
||||
zh: '请求的页面不再可用',
|
||||
id: 'Halaman yang diminta tidak lagi tersedia',
|
||||
},
|
||||
'Length Required': {
|
||||
fr: 'Longueur requise',
|
||||
ru: 'Необходима длина',
|
||||
uk: 'Потрібно вказати довжину',
|
||||
pt: 'Content-Length necessário',
|
||||
nl: 'Lengte benodigd',
|
||||
de: 'Länge benötigt',
|
||||
es: 'Longitud requerida',
|
||||
zh: '需要长度',
|
||||
id: 'Panjang yang diperlukan',
|
||||
},
|
||||
'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',
|
||||
zh: '未指定Content-Length(内容长度)。服务器将不接受不包含此头信息的请求',
|
||||
id: '"Content-Length" tidak ditentukan. Server tidak akan menerima permintaan tanpa itu',
|
||||
},
|
||||
'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',
|
||||
zh: '前置条件判定失败',
|
||||
},
|
||||
'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',
|
||||
zh: '服务器评估请求中给出的前置条件的结果为false(假)',
|
||||
id: 'Prakondisi gagal',
|
||||
},
|
||||
'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',
|
||||
zh: '请求体过大',
|
||||
id: 'Muatan terlalu besar',
|
||||
},
|
||||
'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',
|
||||
zh: '请求体过大,服务器将不接受该请求',
|
||||
id: 'Server tidak akan menerima permintaan, karena entitas permintaan terlalu besar',
|
||||
},
|
||||
'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',
|
||||
zh: '不满足请求范围',
|
||||
id: 'Rentang yang diminta tidak dapat dipenuhi',
|
||||
},
|
||||
'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',
|
||||
zh: '请求的字节范围不可用,超出边界',
|
||||
id: 'Rentang byte yang diminta tidak tersedia dan di luar batas',
|
||||
},
|
||||
'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',
|
||||
zh: '我是一只茶壶',
|
||||
id: 'Saya adalah teko',
|
||||
},
|
||||
'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',
|
||||
zh: '用茶壶泡咖啡不受支持',
|
||||
id: 'Upaya menyeduh kopi dengan teko tidak didukung',
|
||||
},
|
||||
'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',
|
||||
zh: '请求过多',
|
||||
id: 'Terlalu banyak permintaan',
|
||||
},
|
||||
'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',
|
||||
zh: '在给定的时间内发送了过多请求',
|
||||
id: 'Terlalu banyak permintaan dalam waktu tertentu',
|
||||
},
|
||||
'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',
|
||||
zh: '内部服务器错误',
|
||||
id: 'Kesalahan server internal',
|
||||
},
|
||||
'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',
|
||||
zh: '服务器遇到了意外情况',
|
||||
id: 'Server mengalami kondisi yang tidak terduga',
|
||||
},
|
||||
'Bad Gateway': {
|
||||
fr: 'Mauvaise passerelle',
|
||||
ru: 'Ошибка шлюза',
|
||||
uk: 'Помилка шлюзу',
|
||||
pt: 'Gateway inválido',
|
||||
nl: 'Ongeldige Gateway',
|
||||
de: 'Fehlerhafter Gateway',
|
||||
es: 'Puerta de enlace no valida',
|
||||
zh: '无效网关',
|
||||
id: 'Gateway yang buruk',
|
||||
},
|
||||
'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: 'Сервер отримав невірну відповідь від попереднього сервера',
|
||||
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',
|
||||
zh: '服务器从上游服务器收到了无效的响应',
|
||||
id: 'Server menerima respons yang tidak valid dari server induk',
|
||||
},
|
||||
'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',
|
||||
zh: '服务不可用',
|
||||
id: 'Layanan tidak tersedia',
|
||||
},
|
||||
'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',
|
||||
zh: '服务器暂时过载或不可用',
|
||||
id: 'Server untuk sementara kelebihan beban atau tidak tersedia',
|
||||
},
|
||||
'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',
|
||||
zh: '网关超时',
|
||||
id: 'Batas waktu gateway',
|
||||
},
|
||||
'The gateway has timed out': {
|
||||
fr: 'Le temps d’attente de la passerelle est dépassé',
|
||||
ru: 'Сервер не дождался ответа от вышестоящего сервера',
|
||||
uk: 'У шлюзу закінчився час очікування',
|
||||
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',
|
||||
zh: '网关响应已经超时',
|
||||
id: 'Sambungan ke server induk telah kedaluwarsa',
|
||||
},
|
||||
'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',
|
||||
zh: 'HTTP版本不受支持',
|
||||
id: 'Versi HTTP tidak didukung',
|
||||
},
|
||||
'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',
|
||||
zh: '服务器不支持该HTTP协议版本',
|
||||
id: 'Server tidak mendukung versi HTTP ini',
|
||||
},
|
||||
|
||||
'Host': {
|
||||
fr: 'Hôte',
|
||||
ru: 'Хост',
|
||||
uk: 'Хост',
|
||||
pt: 'Hospedeiro',
|
||||
nl: 'Host',
|
||||
de: 'Host',
|
||||
es: 'Host',
|
||||
zh: '主机',
|
||||
id: 'Host',
|
||||
},
|
||||
'Original URI': {
|
||||
fr: 'URI d’origine',
|
||||
ru: 'Исходный URI',
|
||||
uk: 'Вихідний URI',
|
||||
pt: 'URI original',
|
||||
nl: 'Originele URI',
|
||||
de: 'Originale URI',
|
||||
es: 'URI original',
|
||||
zh: '原始URI',
|
||||
id: 'URL asli',
|
||||
},
|
||||
'Forwarded for': {
|
||||
fr: 'Transmis pour',
|
||||
ru: 'Перенаправлен',
|
||||
uk: 'Перенаправлений',
|
||||
pt: 'Encaminhado para',
|
||||
nl: 'Doorgestuurd voor',
|
||||
de: 'Weitergeleitet für',
|
||||
es: 'Remitido por',
|
||||
zh: '转发自',
|
||||
id: 'Diteruskan untuk',
|
||||
},
|
||||
'Namespace': {
|
||||
fr: 'Espace de noms',
|
||||
ru: 'Пространство имён',
|
||||
uk: 'Простір імен',
|
||||
pt: 'Namespace',
|
||||
nl: 'Elementnaam',
|
||||
de: 'Namensraum',
|
||||
es: 'Namespace',
|
||||
zh: '命名空间',
|
||||
id: 'Ruang nama',
|
||||
},
|
||||
'Ingress name': {
|
||||
fr: 'Nom ingress',
|
||||
ru: 'Имя Ingress',
|
||||
uk: 'Ім\'я входу',
|
||||
pt: 'Nome Ingress',
|
||||
nl: 'Ingress naam',
|
||||
de: 'Ingress Name',
|
||||
es: 'Nombre Ingress',
|
||||
zh: '入口名',
|
||||
id: 'Nama ingress',
|
||||
},
|
||||
'Service name': {
|
||||
fr: 'Nom du service',
|
||||
ru: 'Имя сервиса',
|
||||
uk: 'Ім\'я сервісу',
|
||||
pt: 'Nome do Serviço',
|
||||
nl: 'Service naam',
|
||||
de: 'Service Name',
|
||||
es: 'Nombre del servicio',
|
||||
zh: '服务名',
|
||||
id: 'Nama layanan',
|
||||
},
|
||||
'Service port': {
|
||||
fr: 'Port du service',
|
||||
ru: 'Порт сервиса',
|
||||
uk: 'Порт сервісу',
|
||||
pt: 'Porta do serviço',
|
||||
nl: 'Service poort',
|
||||
de: 'Service Port',
|
||||
es: 'Puerto del servicio',
|
||||
zh: '服务端口',
|
||||
id: 'Port layanan',
|
||||
},
|
||||
'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',
|
||||
zh: '请求ID',
|
||||
id: 'ID permintaan',
|
||||
},
|
||||
'Timestamp': {
|
||||
fr: 'Horodatage',
|
||||
ru: 'Временная метка',
|
||||
uk: 'Мітка часу',
|
||||
pt: 'Timestamp',
|
||||
nl: 'Tijdstempel',
|
||||
de: 'Zeitstempel',
|
||||
es: 'Timestamp',
|
||||
zh: '时间戳',
|
||||
id: 'Cap waktu',
|
||||
},
|
||||
|
||||
'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',
|
||||
zh: '客户端错误',
|
||||
id: 'Kesalahan sisi klien',
|
||||
},
|
||||
'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',
|
||||
zh: '服务端错误',
|
||||
id: 'Kesalahan sisi server',
|
||||
},
|
||||
|
||||
'Your Client': {
|
||||
fr: 'Votre Client',
|
||||
ru: 'Ваш Браузер',
|
||||
uk: 'Ваш Браузер',
|
||||
pt: 'Seu Cliente',
|
||||
nl: 'Jouw Client',
|
||||
de: 'Ihr Client',
|
||||
es: 'Tu cliente',
|
||||
zh: '您的客户端',
|
||||
id: 'Klien Anda',
|
||||
},
|
||||
'Network': {
|
||||
fr: 'Réseau',
|
||||
ru: 'Сеть',
|
||||
uk: 'Мережа',
|
||||
pt: 'Rede',
|
||||
nl: 'Netwerk',
|
||||
de: 'Netzwerk',
|
||||
es: 'Red',
|
||||
zh: '网络',
|
||||
id: 'Jaringan',
|
||||
},
|
||||
'Web Server': {
|
||||
fr: 'Serveur Web',
|
||||
ru: 'Web Сервер',
|
||||
uk: 'Web-сервер',
|
||||
pt: 'Servidor web',
|
||||
nl: 'Web Server',
|
||||
de: 'Webserver',
|
||||
es: 'Servidor web',
|
||||
zh: 'Web服务器',
|
||||
id: 'Server 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?',
|
||||
zh: '发生了什么?',
|
||||
id: 'Apa yang terjadi?',
|
||||
},
|
||||
'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?',
|
||||
zh: '我能做什么?',
|
||||
id: 'Apa yang bisa saya lakukan?',
|
||||
},
|
||||
'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',
|
||||
zh: '请在几分钟后重试',
|
||||
id: 'Silakan coba lagi dalam beberapa menit',
|
||||
},
|
||||
'Working': {
|
||||
fr: 'Opérationnel',
|
||||
ru: 'Работает',
|
||||
uk: 'Працює',
|
||||
pt: 'Funcionando',
|
||||
nl: 'Functioneel',
|
||||
de: 'Funktioniert',
|
||||
es: 'Trabajando',
|
||||
zh: '正常运行',
|
||||
id: 'Fungsi',
|
||||
},
|
||||
'Unknown': {
|
||||
fr: 'Inconnu',
|
||||
ru: 'Неизвестно',
|
||||
uk: 'Невідомо',
|
||||
pt: 'Desconhecido',
|
||||
nl: 'Onbekend',
|
||||
de: 'Unbekannt',
|
||||
es: 'Desconocido',
|
||||
zh: '未知',
|
||||
id: 'Tidak diketahui',
|
||||
},
|
||||
'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',
|
||||
zh: '请尝试更改请求方法、标头、有效负载或URL',
|
||||
id: 'Coba lagi dengan metode, header, muatan, atau URL yang berbeda',
|
||||
},
|
||||
'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',
|
||||
zh: '请检查您的授权数据',
|
||||
id: 'Memeriksa detail autentikasi',
|
||||
},
|
||||
'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',
|
||||
zh: '请再次检查URL并重试',
|
||||
id: 'Periksa URL dan coba lagi',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @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();
|
22
l10n/readme.md
Normal file
22
l10n/readme.md
Normal file
@ -0,0 +1,22 @@
|
||||
# 🔤 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)
|
||||
- 🇨🇳 Chinese by [@CDN18](https://github.com/CDN18)
|
||||
- 🇮🇩 Indonesian by [@getwisp](https://github.com/getwisp)
|
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.bunny.net" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/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>
|
123
templates/cats.html
Normal file
123
templates/cats.html
Normal file
@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Error {{ code }}: {{ message }}
|
||||
Description: {{ description }}
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow"/>
|
||||
<title>{{ message }}</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
background-color: #000;
|
||||
color: #ddd;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.centered {
|
||||
height: 100vh;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.centered img {
|
||||
max-width: 750px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* {{ if show_details }} */
|
||||
.details table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.details td {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.details td.name {
|
||||
text-align: right;
|
||||
padding-right: .6em;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.details td.value {
|
||||
text-align: left;
|
||||
padding-left: .6em;
|
||||
font-family: 'Lucida Console', 'Courier New', monospace;
|
||||
}
|
||||
/* {{ end }} */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="centered">
|
||||
<!-- Pictures provider: <https://http.cat/> -->
|
||||
<div>
|
||||
<img src="https://http.cat/{{ code }}.jpg" alt="{{ message }}">
|
||||
{{ if show_details }}
|
||||
<div class="details">
|
||||
<table>
|
||||
{{- if host }}<tr>
|
||||
<td class="name" 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 }}
|
||||
Description: {{ description }}
|
||||
-->
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user