Compare commits

..

60 Commits

Author SHA1 Message Date
0bd989e493 docs(changelog): file updated 2023-01-29 15:49:28 +04:00
59c4d2022c ci: workflow files have been updated 2023-01-29 15:39:49 +04:00
1ec17caa1d feat: Possibility to use custom env variables in templates (#165) 2023-01-29 15:25:38 +04:00
252618a975 chore: Better CLI (#163) 2023-01-29 14:54:56 +04:00
315c7660d1 Update README.md 2022-12-27 11:41:51 +04:00
6a6809b07f Bump go.uber.org/zap from 1.23.0 to 1.24.0 (#159) 2022-12-02 06:09:55 +00:00
a960b5928e Bump github.com/valyala/fasthttp from 1.41.0 to 1.43.0 (#157) 2022-12-02 06:06:16 +00:00
46d96d7bb4 Bump github.com/prometheus/client_golang from 1.13.0 to 1.14.0 (#160) 2022-12-02 06:02:10 +00:00
69063a9cf7 Bump github.com/fasthttp/router from 1.4.13 to 1.4.14 (#158) 2022-12-02 06:01:28 +00:00
7c8d2f54c7 Update orangeopensource/hurl Docker tag to v1.8.0 (#155)
* Update orangeopensource/hurl Docker tag to v1.8.0

* wip: temporary commit

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2022-11-18 23:46:46 +04:00
49aed23f8c Update README.md 2022-11-15 13:06:39 +04:00
3418f85292 Update CHANGELOG.md 2022-11-02 01:38:14 +04:00
361afd87aa Bump github.com/stretchr/testify from 1.8.0 to 1.8.1 (#152)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-02 01:33:17 +04:00
7f6815c274 Bump github.com/spf13/cobra from 1.5.0 to 1.6.1 (#151) 2022-11-01 21:31:51 +00:00
4ecd70330a Bump aquasecurity/trivy-action from 0.7.1 to 0.8.0 (#148) 2022-11-01 21:28:27 +00:00
ae13905512 Bump github.com/prometheus/client_model from 0.2.0 to 0.3.0 (#149) 2022-11-01 21:27:39 +00:00
e3377d0f28 Bump github.com/fasthttp/router from 1.4.12 to 1.4.13 (#153) 2022-11-01 21:27:23 +00:00
b15061a110 wip: Readme file updated 2022-11-02 01:25:19 +04:00
CDN
6d6945bf44 Add 🇨🇳 Chinese Translation (#147) 2022-11-02 01:23:29 +04:00
cf7c526d4f golangci-lint issues fixed 2022-11-02 01:13:03 +04:00
8e21be0340 Update golangci/golangci-lint Docker tag to v1.50 (#144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-10-04 15:41:42 +04:00
37265ccb4f Bump github.com/fasthttp/router from 1.4.11 to 1.4.12 (#142) 2022-10-02 05:28:04 +00:00
169fbe3b93 Bump github.com/valyala/fasthttp from 1.39.0 to 1.40.0 (#143) 2022-10-02 02:22:55 +00:00
617b378c36 Update orangeopensource/hurl Docker tag to v1.7.0 (#141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-09-24 10:31:48 +04:00
438e954dd6 Bump go.uber.org/zap from 1.21.0 to 1.23.0 (#135) 2022-09-02 02:57:20 +00:00
df1a0e20ee Bump aquasecurity/trivy-action from 0.6.1 to 0.7.1 (#137) 2022-09-02 02:53:52 +00:00
7b3c286790 Bump github.com/valyala/fasthttp from 1.38.0 to 1.39.0 (#136) 2022-09-02 02:53:34 +00:00
fb7d7c75cf Bump github.com/prometheus/client_golang from 1.12.2 to 1.13.0 (#138) 2022-09-02 02:52:41 +00:00
1eafe58d16 Update golang Docker tag to v1.19 (#127)
* Update golang Docker tag to v1.19

* wip: temporary commit

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2022-08-24 21:38:26 +04:00
e7a909dc4e Update golangci/golangci-lint Docker tag to v1.49 (#134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-24 20:38:41 +04:00
9deee9ddba replace google by bunny fonts (#131) 2022-08-19 23:19:10 +04:00
e769c2103f Update dependency golangci/golangci-lint to v1.48 (#128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-08-05 06:05:19 +04:00
ca9cdf0379 Bump github.com/fasthttp/router from 1.4.10 to 1.4.11 (#125) 2022-08-02 04:56:43 +00:00
7ef471381c Bump aquasecurity/trivy-action from 0.5.1 to 0.6.1 (#126) 2022-08-02 04:56:24 +00:00
48e9b20836 go get -u golang.org/x/sys 2022-07-26 21:49:19 +04:00
7329d7697c Readme & changelog files are updated 2022-07-25 11:13:18 +04:00
83f38cdd16 Spanish Translation (#124) 2022-07-25 11:05:06 +04:00
057006366d Update dependency golangci/golangci-lint to v1.47 (#123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-18 19:32:55 +04:00
f08539962e Bump github.com/valyala/fasthttp from 1.37.0 to 1.38.0 (#122) 2022-07-02 11:06:00 +00:00
2cc8549cef Bump github.com/spf13/cobra from 1.4.0 to 1.5.0 (#121) 2022-07-02 11:02:45 +00:00
8f8e5abd3d Bump github.com/fasthttp/router from 1.4.9 to 1.4.10 (#120) 2022-07-02 10:59:19 +00:00
1889a57c05 Bump github.com/stretchr/testify from 1.7.1 to 1.8.0 (#119) 2022-07-02 10:58:56 +00:00
c42ff85dd6 Bump aquasecurity/trivy-action from 0.3.0 to 0.5.1 (#118) 2022-07-02 10:58:42 +00:00
00d3c10e5e Update tests.yml 2022-06-12 13:38:17 +05:00
7153c260d8 Update dependency orangeopensource/hurl to v1.6.1 (#117)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-06-12 13:11:52 +05:00
48bd1a44e6 Configure Renovate (#116)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2022-06-12 12:55:49 +05:00
bde35e2c79 Bump golang from 1.18.1-alpine to 1.18.3-alpine (#114) 2022-06-05 09:22:08 +00:00
d649e371a5 Update CHANGELOG.md 2022-06-04 17:11:20 +05:00
01c2a37055 Add 🇩🇪 German translation (#115)
* german translation

* update readmes and changelog
2022-06-04 17:06:39 +05:00
a932f94ec0 Bump docker/setup-buildx-action from 1 to 2 (#111) 2022-06-02 05:14:32 +00:00
3ac2c74249 Bump github.com/valyala/fasthttp from 1.36.0 to 1.37.0 (#106) 2022-06-02 05:14:08 +00:00
18af96bada Bump github.com/prometheus/client_golang from 1.12.1 to 1.12.2 (#108) 2022-06-02 05:10:46 +00:00
445aad8b41 Bump docker/login-action from 1 to 2 (#109) 2022-06-02 05:10:27 +00:00
b61cc7460f Bump github.com/fasthttp/router from 1.4.8 to 1.4.9 (#110) 2022-06-02 05:10:14 +00:00
c9586fe79a Bump aquasecurity/trivy-action from 0.2.5 to 0.3.0 (#112) 2022-06-02 05:09:47 +00:00
405afec38a Bump docker/setup-qemu-action from 1 to 2 (#107) 2022-06-02 05:09:16 +00:00
5e0be010b7 Bump docker/build-push-action from 2 to 3 (#113) 2022-06-02 05:09:00 +00:00
9bc00fa4ca Update release.yml 2022-05-12 17:20:02 +05:00
6742381562 Update tests.yml 2022-05-12 17:18:45 +05:00
6d3ced480d Add 🇳🇱 Dutch translation (#104)
Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2022-05-07 15:09:23 +05:00
49 changed files with 1517 additions and 864 deletions

View File

@ -10,7 +10,7 @@ indent_style = space
indent_size = 4 indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.{yml, yaml, sh, conf}] [*.{yml, yaml, sh, conf, json}]
indent_size = 2 indent_size = 2
[{Makefile, go.mod, *.go}] [{Makefile, go.mod, *.go}]

7
.github/renovate.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>tarampampam/.github//renovate/default",
":rebaseStalePrs"
]
}

View File

@ -27,7 +27,7 @@ jobs:
arch: [amd64] # amd64, 386 arch: [amd64] # amd64, 386
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: {go-version: 1.18.1} with: {go-version: 1.19}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -63,22 +63,22 @@ jobs:
- uses: gacts/github-slug@v1 - uses: gacts/github-slug@v1
id: slug 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: with:
username: ${{ secrets.DOCKER_LOGIN }} username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_PASSWORD }} 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: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GHCR_PASSWORD }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action> - uses: docker/build-push-action@v3 # Action page: <https://github.com/docker/build-push-action>
with: with:
context: . context: .
file: Dockerfile file: Dockerfile

View File

@ -8,6 +8,10 @@ on:
pull_request: pull_request:
paths-ignore: ['**.md'] paths-ignore: ['**.md']
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
jobs: # Docs: <https://git.io/JvxXE> jobs: # Docs: <https://git.io/JvxXE>
gitleaks: gitleaks:
name: Gitleaks name: Gitleaks
@ -16,7 +20,8 @@ jobs: # Docs: <https://git.io/JvxXE>
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: {fetch-depth: 0} 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: golangci-lint:
name: Golang-CI (lint) name: Golang-CI (lint)
@ -24,14 +29,11 @@ jobs: # Docs: <https://git.io/JvxXE>
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: gacts/setup-go-with-cache@v1
with: {go-version: 1.17} # On v1.18 I had an error "panic: load embedded ruleguard rules: rules/rules.go:13: can't load fmt" with: {go-version: 1.19}
- name: Run linter - uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v3 # Action page: <https://github.com/golangci/golangci-lint-action> with: {skip-pkg-cache: true, skip-build-cache: true}
with:
version: v1.44 # without patch version
only-new-issues: false # show only new issues if it's a pull request
validate-config-file: validate-config-file:
name: Validate config file name: Validate config file
@ -69,7 +71,7 @@ jobs: # Docs: <https://git.io/JvxXE>
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: {go-version: 1.18} with: {go-version: 1.19}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: {fetch-depth: 2} # Fixes codecov error 'Issue detecting commit SHA' with: {fetch-depth: 2} # Fixes codecov error 'Issue detecting commit SHA'
@ -105,7 +107,7 @@ jobs: # Docs: <https://git.io/JvxXE>
needs: [golangci-lint, go-test, validate-config-file] needs: [golangci-lint, go-test, validate-config-file]
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: {go-version: 1.18} with: {go-version: 1.19}
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -131,7 +133,7 @@ jobs: # Docs: <https://git.io/JvxXE>
- name: Try to execute - name: Try to execute
if: matrix.os == 'linux' if: matrix.os == 'linux'
run: ./error-pages version && ./error-pages -h run: ./error-pages --version && ./error-pages -h
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
@ -157,7 +159,7 @@ jobs: # Docs: <https://git.io/JvxXE>
run: mv ./error-pages ./../error-pages && chmod +x ./../error-pages run: mv ./error-pages ./../error-pages && chmod +x ./../error-pages
- name: Run generator - name: Run generator
run: ./error-pages build ./out --verbose --index run: ./error-pages --verbose build --index ./out
- name: Test files creation - name: Test files creation
run: | run: |
@ -184,7 +186,7 @@ jobs: # Docs: <https://git.io/JvxXE>
- uses: gacts/github-slug@v1 - uses: gacts/github-slug@v1
id: slug id: slug
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action> - uses: docker/build-push-action@v3 # Action page: <https://github.com/docker/build-push-action>
with: with:
context: . context: .
file: Dockerfile file: Dockerfile
@ -212,7 +214,7 @@ jobs: # Docs: <https://git.io/JvxXE>
name: docker-image name: docker-image
path: .artifact path: .artifact
- uses: aquasecurity/trivy-action@0.2.5 # action page: <https://github.com/aquasecurity/trivy-action> - uses: aquasecurity/trivy-action@0.8.0 # action page: <https://github.com/aquasecurity/trivy-action>
with: with:
input: .artifact/docker-image.tar input: .artifact/docker-image.tar
format: sarif format: sarif
@ -241,13 +243,7 @@ jobs: # Docs: <https://git.io/JvxXE>
- working-directory: .artifact - working-directory: .artifact
run: docker load < docker-image.tar run: docker load < docker-image.tar
- name: Download hurl - uses: gacts/install-hurl@v1
env:
VERSION: 1.5.0
run: curl -SL -o hurl.deb https://github.com/Orange-OpenSource/hurl/releases/download/${VERSION}/hurl_${VERSION}_amd64.deb
- name: Install hurl
run: sudo dpkg -i hurl.deb
- name: Run container with the app - name: Run container with the app
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 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
@ -255,7 +251,7 @@ jobs: # Docs: <https://git.io/JvxXE>
- name: Wait for container "healthy" state - 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: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done
- run: hurl --color --test --fail-at-end --variable host=127.0.0.1 --variable port=8080 --summary ./test/hurl/*.hurl - run: hurl --color --test --fail-at-end --variable host=127.0.0.1 --variable port=8080 ./test/hurl/*.hurl
- name: Stop the container - name: Stop the container
if: always() if: always()

View File

@ -43,9 +43,6 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
enable: enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bidichk # Checks for dangerous unicode character sequences - bidichk # Checks for dangerous unicode character sequences
- bodyclose # Checks whether HTTP response body is closed successfully
- contextcheck # check the function whether use a non-inherited context
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages - depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection - dupl # Tool for code clone detection
@ -73,19 +70,13 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
- nakedret # Finds naked returns in functions greater than a specified function length - nakedret # Finds naked returns in functions greater than a specified function length
- nestif # Reports deeply nested if statements - nestif # Reports deeply nested if statements
- nlreturn # checks for a new line before return and branch statements to increase code clarity - 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 - nolintlint # Reports ill-formed or insufficient nolint directives
- prealloc # Finds slice declarations that could potentially be preallocated - 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 - 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 - 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 - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- unconvert # Remove unnecessary type conversions - unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types - 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 - whitespace # Tool for detection of leading and trailing whitespace
- wsl # Whitespace Linter - Forces you to use empty lines! - wsl # Whitespace Linter - Forces you to use empty lines!
@ -97,4 +88,3 @@ issues:
- funlen - funlen
- scopelint - scopelint
- gocognit - gocognit
- noctx

View File

@ -4,11 +4,71 @@ 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]. The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver].
## 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 ## v2.14.0
### Added ### Added
- Error pages now translated in portuguese 🇵🇹 [#103] - Error pages now translated into Portuguese 🇵🇹 [#103]
### Changed ### Changed
@ -41,7 +101,7 @@ The format is based on [Keep a Changelog][keepachangelog] and this project adher
### Changed ### Changed
- Error pages now translated in 🇫🇷 [#82] - Error pages now translated into 🇫🇷 [#82]
[#82]:https://github.com/tarampampam/error-pages/pull/82 [#82]:https://github.com/tarampampam/error-pages/pull/82
@ -68,7 +128,7 @@ The format is based on [Keep a Changelog][keepachangelog] and this project adher
### Changed ### Changed
- Error pages now translated in 🇺🇦 and 🇷🇺 languages [#80] - Error pages now translated into 🇺🇦 and 🇷🇺 languages [#80]
[#80]:https://github.com/tarampampam/error-pages/pull/80 [#80]:https://github.com/tarampampam/error-pages/pull/80

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.2 # syntax=docker/dockerfile:1.2
# Image page: <https://hub.docker.com/_/golang> # Image page: <https://hub.docker.com/_/golang>
FROM golang:1.18.1-alpine as builder FROM golang:1.19-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" .` # 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" ARG APP_VERSION="undefined@docker"
@ -16,7 +16,7 @@ ENV LDFLAGS="-s -w -X github.com/tarampampam/error-pages/internal/version.versio
RUN set -x \ RUN set -x \
&& go version \ && go version \
&& CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/ \ && CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/ \
&& ./error-pages version \ && ./error-pages --version \
&& ./error-pages -h && ./error-pages -h
WORKDIR /tmp/rootfs WORKDIR /tmp/rootfs
@ -38,7 +38,7 @@ WORKDIR /tmp/rootfs/opt
# generate static error pages (for usage inside another docker images, for example) # generate static error pages (for usage inside another docker images, for example)
RUN set -x \ 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 && ls -l ./html
# use empty filesystem # use empty filesystem
@ -72,8 +72,8 @@ ENV LISTEN_PORT="8080" \
DISABLE_L10N="false" DISABLE_L10N="false"
# Docs: <https://docs.docker.com/engine/reference/builder/#healthcheck> # 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"] ENTRYPOINT ["/bin/error-pages"]
CMD ["serve", "--log-json"] CMD ["--log-json", "serve"]

View File

@ -43,7 +43,7 @@ gotest: ## Run app tests
docker-compose run $(DC_RUN_ARGS) --no-deps app go test -v -race -timeout 10s ./... docker-compose run $(DC_RUN_ARGS) --no-deps app go test -v -race -timeout 10s ./...
int-test: ## Run integration tests (docs: https://hurl.dev/docs/man-page.html#options) int-test: ## Run integration tests (docs: https://hurl.dev/docs/man-page.html#options)
docker-compose run --rm hurl --color --test --fail-at-end --variable host=web --variable port=8080 --summary ./test/hurl/*.hurl 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 test: lint gotest int-test ## Run app tests and linters

View File

@ -5,8 +5,8 @@
<p align="center"> <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="#"><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://codecov.io/gh/tarampampam/error-pages"><img src="https://img.shields.io/codecov/c/github/tarampampam/error-pages/master.svg?maxAge=30&label=&logo=codecov&logoColor=white&style=flat-square" alt="" /></a>
<a href="https://github.com/tarampampam/error-pages/actions"><img src="https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests?maxAge=30&label=tests&logo=github&style=flat-square" alt="" /></a> <a href="https://github.com/tarampampam/error-pages/actions"><img src="https://img.shields.io/github/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/workflow/status/tarampampam/error-pages/release?maxAge=30&label=release&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/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://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> <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>
@ -34,14 +34,12 @@ One day you may want to replace the standard error pages of your HTTP server wit
- Error pages can be [embedded into your own `nginx`][wiki-usage-with-nginx] docker image - 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]) - 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 - 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!) - Localized (🇺🇸, 🇫🇷, 🇺🇦, 🇷🇺, 🇵🇹, 🇳🇱, 🇩🇪, 🇪🇸, 🇨🇳) HTML error pages (translation process [described here](https://github.com/tarampampam/error-pages/tree/master/l10n) - other translations are welcome!)
## 🧩 Install ## 🧩 Install
Download the latest binary file for your os/arch from the [releases page][releases] or use our docker image: Download the latest binary file for your os/arch from the [releases page][releases] or use our docker image:
[![image stats](https://dockeri.co/image/tarampampam/error-pages)][docker-hub-tags]
| Registry | Image | | Registry | Image |
|-----------------------------------|-----------------------------------| |-----------------------------------|-----------------------------------|
| [Docker Hub][docker-hub] | `tarampampam/error-pages` | | [Docker Hub][docker-hub] | `tarampampam/error-pages` |

View File

@ -1,13 +1,22 @@
package main package main
import ( import (
crypto "crypto/rand"
"encoding/binary"
"math/rand"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/fatih/color" "github.com/fatih/color"
"go.uber.org/automaxprocs/maxprocs"
"github.com/tarampampam/error-pages/internal/cli" "github.com/tarampampam/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. // exitFn is a function for application exiting.
var exitFn = os.Exit //nolint:gochecknoglobals var exitFn = os.Exit //nolint:gochecknoglobals
@ -17,9 +26,16 @@ func main() { exitFn(run()) }
// run this CLI application. // run this CLI application.
// Exit codes documentation: <https://tldp.org/LDP/abs/html/exitcodes.html> // Exit codes documentation: <https://tldp.org/LDP/abs/html/exitcodes.html>
func run() int { func run() int {
cmd := cli.NewCommand(filepath.Base(os.Args[0])) var b [8]byte
if err := cmd.Execute(); err != nil { // seed random number generator
if _, err := crypto.Read(b[:]); err == nil {
rand.Seed(int64(binary.LittleEndian.Uint64(b[:]))) // https://stackoverflow.com/a/54491783/2252921
} else {
rand.Seed(time.Now().UnixNano()) // fallback
}
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()) _, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error())
return 1 return 1

View File

@ -6,36 +6,16 @@ import (
"github.com/kami-zh/go-capturer" "github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func Test_Main(t *testing.T) { func Test_MainHelp(t *testing.T) {
os.Args = []string{"", "--help"} os.Args = []string{"", "--help"}
exitFn = func(code int) { assert.Equal(t, 0, code) } exitFn = func(code int) { require.Equal(t, 0, code) }
output := capturer.CaptureStdout(main) output := capturer.CaptureStdout(main)
assert.Contains(t, output, "Usage:") assert.Contains(t, output, "USAGE:")
assert.Contains(t, output, "Available Commands:") assert.Contains(t, output, "COMMANDS:")
assert.Contains(t, output, "Flags:") assert.Contains(t, output, "GLOBAL OPTIONS:")
}
func Test_MainWithoutCommands(t *testing.T) {
os.Args = []string{""}
exitFn = func(code int) { assert.Equal(t, 0, code) }
output := capturer.CaptureStdout(main)
assert.Contains(t, output, "Usage:")
assert.Contains(t, output, "Available Commands:")
assert.Contains(t, output, "Flags:")
}
func Test_MainUnknownSubcommand(t *testing.T) {
os.Args = []string{"", "foobar"}
exitFn = func(code int) { assert.Equal(t, 1, code) }
output := capturer.CaptureStderr(main)
assert.Contains(t, output, "unknown command")
assert.Contains(t, output, "foobar")
} }

View File

@ -8,7 +8,7 @@ volumes:
services: services:
app: &app-service app: &app-service
image: golang:1.18.1-buster # Image page: <https://hub.docker.com/_/golang> image: golang:1.19-buster # Image page: <https://hub.docker.com/_/golang>
working_dir: /src working_dir: /src
environment: environment:
HOME: /tmp HOME: /tmp
@ -23,22 +23,15 @@ services:
<<: *app-service <<: *app-service
ports: ports:
- "8080:8080/tcp" # Open <http://127.0.0.1:8080> - "8080:8080/tcp" # Open <http://127.0.0.1:8080>
command: command: sh -c "go build -o /tmp/app ./cmd/error-pages && /tmp/app serve"
- go
- run
- ./cmd/error-pages
- serve
- --verbose
- --port=8080
- --show-details
- --proxy-headers=X-Foo,Bar,Baz_blah
healthcheck: healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/healthz'] test: ['CMD', '/tmp/app', 'healthcheck', '--log-json']
interval: 5s interval: 4s
timeout: 2s timeout: 1s
start_period: 5s
golint: golint:
image: golangci/golangci-lint:v1.44-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint> image: golangci/golangci-lint:v1.50-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint>
environment: environment:
GOLANGCI_LINT_CACHE: /tmp/golint # <https://github.com/golangci/golangci-lint/blob/v1.42.0/internal/cache/default.go#L68> GOLANGCI_LINT_CACHE: /tmp/golint # <https://github.com/golangci/golangci-lint/blob/v1.42.0/internal/cache/default.go#L68>
volumes: volumes:
@ -48,10 +41,9 @@ services:
command: /bin/true command: /bin/true
hurl: hurl:
image: orangeopensource/hurl:1.5.0 image: orangeopensource/hurl:1.8.0
volumes: volumes:
- .:/src:ro - .:/src:ro
working_dir: /src working_dir: /src
depends_on: depends_on:
web: web: {condition: service_healthy}
condition: service_healthy

37
go.mod
View File

@ -1,41 +1,44 @@
module github.com/tarampampam/error-pages module github.com/tarampampam/error-pages
go 1.18 go 1.19
require ( require (
github.com/a8m/envsubst v1.3.0 github.com/a8m/envsubst v1.3.0
github.com/fasthttp/router v1.4.8 github.com/fasthttp/router v1.4.14
github.com/fatih/color v1.13.0 github.com/fatih/color v1.13.0
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.12.1 github.com/prometheus/client_golang v1.14.0
github.com/prometheus/client_model v0.2.0 github.com/prometheus/client_model v0.3.0
github.com/spf13/cobra v1.4.0 github.com/stretchr/testify v1.8.1
github.com/spf13/pflag v1.0.5 github.com/urfave/cli/v2 v2.24.1
github.com/stretchr/testify v1.7.1 github.com/valyala/fasthttp v1.43.0
github.com/valyala/fasthttp v1.36.0 go.uber.org/automaxprocs v1.5.1
go.uber.org/zap v1.21.0 go.uber.org/goleak v1.1.11
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b go.uber.org/zap v1.24.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/savsgio/gotils v0.0.0-20220401102855-e56b59f40436 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
) )

82
go.sum
View File

@ -43,7 +43,6 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -57,7 +56,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 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/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-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -65,8 +65,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
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.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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp/router v1.4.8 h1:4zj4sAzXibjA6ZW19MdMe3GaYD1SM+TXrMLzHcVMBOI= github.com/fasthttp/router v1.4.14 h1:+W65VCKgyI4BZszhDiCRfONoFieePZIoQ7D8vGhiuzM=
github.com/fasthttp/router v1.4.8/go.mod h1:UUtJdXFYlqYRQ32EAtWOvNYIZ1XfyC5JJIknWai6foI= github.com/fasthttp/router v1.4.14/go.mod h1:+svLaOvqF9Lc0yjX9aHAD4NUMf+mggLPOT4UMdS6fjM=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -75,9 +75,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -118,8 +120,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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 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.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -137,8 +139,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -151,8 +151,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
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 h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0= github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@ -182,53 +182,63 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/savsgio/gotils v0.0.0-20220401102855-e56b59f40436 h1:sfTahD3f2BSjx9U3R4K09PkNuZZWthT7g6vzTIXNWkM= github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo=
github.com/savsgio/gotils v0.0.0-20220401102855-e56b59f40436/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli/v2 v2.24.1 h1:/QYYr7g0EhwXEML8jO+8OYt5trPnLHS0p3mrgExJ5NU=
github.com/urfave/cli/v2 v2.24.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= github.com/valyala/fasthttp v1.42.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/fasthttp v1.36.0 h1:NhqfO/cB7Ajn1czkKnWkMHyPYr5nyND14ZGPk23g0/c= github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
github.com/valyala/fasthttp v1.36.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
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=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -240,12 +250,14 @@ 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.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk=
go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -274,6 +286,7 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/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-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-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/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-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/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@ -315,13 +328,16 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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-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-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-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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -375,8 +391,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -429,11 +445,11 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 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-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-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.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.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -510,8 +526,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -522,13 +538,11 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.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-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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-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-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-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -3,11 +3,12 @@ package checkers_test
import ( import (
"bytes" "bytes"
"context" "context"
"io/ioutil" "io"
"net/http" "net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/checkers" "github.com/tarampampam/error-pages/internal/checkers"
) )
@ -22,7 +23,7 @@ func TestHealthChecker_CheckSuccess(t *testing.T) {
assert.Equal(t, "HealthChecker/internal", req.Header.Get("User-Agent")) assert.Equal(t, "HealthChecker/internal", req.Header.Get("User-Agent"))
return &http.Response{ return &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte{})), Body: io.NopCloser(bytes.NewReader([]byte{})),
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
}, nil }, nil
} }
@ -35,7 +36,7 @@ func TestHealthChecker_CheckSuccess(t *testing.T) {
func TestHealthChecker_CheckFail(t *testing.T) { func TestHealthChecker_CheckFail(t *testing.T) {
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) { var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
return &http.Response{ return &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte{})), Body: io.NopCloser(bytes.NewReader([]byte{})),
StatusCode: http.StatusBadGateway, StatusCode: http.StatusBadGateway,
}, nil }, nil
} }

103
internal/cli/app.go Normal file
View File

@ -0,0 +1,103 @@
package cli
import (
"context"
"fmt"
"runtime"
"strings"
"github.com/urfave/cli/v2"
"github.com/tarampampam/error-pages/internal/checkers"
"github.com/tarampampam/error-pages/internal/cli/build"
"github.com/tarampampam/error-pages/internal/cli/healthcheck"
"github.com/tarampampam/error-pages/internal/cli/serve"
"github.com/tarampampam/error-pages/internal/env"
"github.com/tarampampam/error-pages/internal/logger"
"github.com/tarampampam/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
View File

@ -0,0 +1,19 @@
package cli_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/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"}))
}

View File

@ -5,60 +5,59 @@ import (
"path" "path"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/urfave/cli/v2"
"go.uber.org/zap"
"github.com/tarampampam/error-pages/internal/cli/shared"
"github.com/tarampampam/error-pages/internal/config" "github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/tpl" "github.com/tarampampam/error-pages/internal/tpl"
"go.uber.org/zap"
) )
type command struct {
c *cli.Command
}
// NewCommand creates `build` command. // NewCommand creates `build` command.
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { func NewCommand(log *zap.Logger) *cli.Command {
var ( var cmd = command{}
generateIndex bool
disableL10n bool const (
cfg *config.Config generateIndexFlagName = "index"
disableL10nFlagName = "disable-l10n"
) )
cmd := &cobra.Command{ cmd.c = &cli.Command{
Use: "build <output-directory>", Name: "build",
Aliases: []string{"b"}, Aliases: []string{"b"},
Short: "Build the error pages", Usage: "build <output-directory>",
Args: cobra.ExactArgs(1), Description: "Build the error pages",
PreRunE: func(*cobra.Command, []string) (err error) { Action: func(c *cli.Context) error {
if configFile == nil { cfg, cfgErr := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name))
return errors.New("path to the config file is required for this command") if cfgErr != nil {
return cfgErr
} }
if cfg, err = config.FromYamlFile(*configFile); err != nil { if c.Args().Len() != 1 {
return err
}
return
},
RunE: func(_ *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("wrong arguments count") return errors.New("wrong arguments count")
} }
return run(log, cfg, args[0], generateIndex, disableL10n) 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( return cmd.c
&generateIndex,
"index", "i",
false,
"generate index page",
)
cmd.Flags().BoolVarP(
&disableL10n,
"disable-l10n", "",
false,
"disable error pages localization",
)
return cmd
} }
const ( const (
@ -68,14 +67,14 @@ const (
outDirPerm = os.FileMode(0775) outDirPerm = os.FileMode(0775)
) )
func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex, disableL10n bool) error { //nolint:funlen,lll func (cmd *command) Run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex, disableL10n bool) error { //nolint:funlen,lll
if len(cfg.Templates) == 0 { if len(cfg.Templates) == 0 {
return errors.New("no loaded templates") return errors.New("no loaded templates")
} }
log.Info("output directory preparing", zap.String("path", outDirectoryPath)) log.Info("output directory preparing", zap.String("path", outDirectoryPath))
if err := createDirectory(outDirectoryPath, outDirPerm); err != nil { if err := cmd.createDirectory(outDirectoryPath, outDirPerm); err != nil {
return errors.Wrap(err, "cannot prepare output directory") return errors.Wrap(err, "cannot prepare output directory")
} }
@ -86,7 +85,7 @@ func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateI
log.Debug("template processing", zap.String("name", template.Name())) log.Debug("template processing", zap.String("name", template.Name()))
for _, page := range cfg.Pages { for _, page := range cfg.Pages {
if err := createDirectory(path.Join(outDirectoryPath, template.Name()), outDirPerm); err != nil { if err := cmd.createDirectory(path.Join(outDirectoryPath, template.Name()), outDirPerm); err != nil {
return err return err
} }
@ -138,7 +137,7 @@ func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateI
return nil return nil
} }
func createDirectory(path string, perm os.FileMode) error { func (cmd *command) createDirectory(path string, perm os.FileMode) error {
stat, err := os.Stat(path) stat, err := os.Stat(path)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {

View File

@ -1,7 +1,26 @@
package build_test package build_test
import "testing" import (
"flag"
"testing"
func TestNothing(t *testing.T) { "github.com/stretchr/testify/assert"
t.Skip("tests for this package have not been implemented yet") "github.com/urfave/cli/v2"
"go.uber.org/goleak"
"go.uber.org/zap"
"github.com/tarampampam/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")
} }

View File

@ -2,56 +2,35 @@
package healthcheck package healthcheck
import ( import (
"fmt" "errors"
"strconv" "math"
"github.com/spf13/cobra" "github.com/urfave/cli/v2"
"github.com/spf13/pflag"
"github.com/tarampampam/error-pages/internal/env" "github.com/tarampampam/error-pages/internal/cli/shared"
) )
type checker interface { type checker interface {
Check(port uint16) error Check(port uint16) error
} }
const portFlagName = "port"
// NewCommand creates `healthcheck` command. // NewCommand creates `healthcheck` command.
func NewCommand(checker checker) *cobra.Command { func NewCommand(checker checker) *cli.Command {
var port uint16 return &cli.Command{
Name: "healthcheck",
cmd := &cobra.Command{
Use: "healthcheck",
Aliases: []string{"chk", "health", "check"}, Aliases: []string{"chk", "health", "check"},
Short: "Health checker for the HTTP server. Use case - docker healthcheck", Usage: "Health checker for the HTTP server. Use case - docker healthcheck",
PreRunE: func(c *cobra.Command, _ []string) (lastErr error) { Action: func(c *cli.Context) error {
c.Flags().VisitAll(func(flag *pflag.Flag) { var port = c.Uint(shared.ListenPortFlag.Name)
// 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)
}
}
}
})
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 { Flags: []cli.Flag{
return checker.Check(port) 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
} }

View File

@ -2,12 +2,12 @@ package healthcheck_test
import ( import (
"errors" "errors"
"os" "flag"
"testing" "testing"
"github.com/kami-zh/go-capturer"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"github.com/tarampampam/error-pages/internal/cli/healthcheck" "github.com/tarampampam/error-pages/internal/cli/healthcheck"
) )
@ -18,77 +18,30 @@ func (c *fakeChecker) Check(port uint16) error { return c.err }
func TestProperties(t *testing.T) { func TestProperties(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil}) 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.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases)
assert.NotNil(t, cmd.RunE) assert.NotNil(t, cmd.Action)
} }
func TestCommandRun(t *testing.T) { func TestCommandRun(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil}) cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
cmd.SetArgs([]string{})
output := capturer.CaptureOutput(func() { assert.NoError(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)))
assert.NoError(t, cmd.Execute())
})
assert.Empty(t, output)
} }
func TestCommandRunFailed(t *testing.T) { func TestCommandRunFailed(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: errors.New("foo err")}) cmd := healthcheck.NewCommand(&fakeChecker{err: errors.New("foo err")})
cmd.SetArgs([]string{})
output := capturer.CaptureStderr(func() { assert.ErrorContains(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)), "foo err")
assert.Error(t, cmd.Execute())
})
assert.Contains(t, output, "foo err")
} }
func TestPortFlagWrongArgument(t *testing.T) { func TestPortFlagWrongArgument(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil}) 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 { assert.ErrorContains(t, err, "port value out of range")
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)
} }

View File

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

View File

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

View File

@ -3,53 +3,164 @@ package serve
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net"
"os" "os"
"strings"
"time" "time"
"github.com/spf13/cobra" "github.com/urfave/cli/v2"
"github.com/tarampampam/error-pages/internal/breaker"
"github.com/tarampampam/error-pages/internal/config"
appHttp "github.com/tarampampam/error-pages/internal/http"
"github.com/tarampampam/error-pages/internal/pick"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/tarampampam/error-pages/internal/breaker"
"github.com/tarampampam/error-pages/internal/cli/shared"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/env"
appHttp "github.com/tarampampam/error-pages/internal/http"
"github.com/tarampampam/error-pages/internal/options"
"github.com/tarampampam/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"
)
const (
useRandomTemplate = "random"
useRandomTemplateOnEachRequest = "i-said-random"
useRandomTemplateDaily = "random-daily"
useRandomTemplateHourly = "random-hourly"
) )
// NewCommand creates `serve` command. // NewCommand creates `serve` command.
func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command { func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
var ( var cmd = command{}
f flags
cfg *config.Config
)
cmd := &cobra.Command{ cmd.c = &cli.Command{
Use: "serve", Name: "serve",
Aliases: []string{"s", "server"}, Aliases: []string{"s", "server"},
Short: "Start HTTP server", Usage: "Start HTTP server",
PreRunE: func(cmd *cobra.Command, _ []string) (err error) { Action: func(c *cli.Context) error {
if configFile == nil { 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") return errors.New("path to the config file is required for this command")
} } else if loadedCfg, err := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name)); err != nil {
if err = f.OverrideUsingEnv(cmd.Flags()); err != nil {
return err return err
} else {
cfg = loadedCfg
} }
if cfg, err = config.FromYamlFile(*configFile); err != nil { var (
return err 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)
} }
return f.Validate() { // 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)
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()},
},
}, },
RunE: func(*cobra.Command, []string) error { return run(ctx, log, cfg, f) },
} }
f.Init(cmd.Flags()) return cmd.c
return cmd
} }
// run current command. // Run current command.
func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags) error { //nolint:funlen func (cmd *command) Run( //nolint:funlen
parentCtx context.Context, log *zap.Logger, cfg *config.Config, ip string, port uint16, opt options.ErrorPage,
) error {
var ( var (
ctx, cancel = context.WithCancel(parentCtx) // serve context creation ctx, cancel = context.WithCancel(parentCtx) // serve context creation
oss = breaker.NewOSSignals(ctx) // OS signals listener oss = breaker.NewOSSignals(ctx) // OS signals listener
@ -70,8 +181,6 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
var ( var (
templateNames = cfg.TemplateNames() templateNames = cfg.TemplateNames()
picker interface{ Pick() string } picker interface{ Pick() string }
opt = f.ToOptions()
) )
switch opt.Template.Name { switch opt.Template.Name {
@ -124,8 +233,8 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
defer close(errCh) defer close(errCh)
log.Info("Server starting", log.Info("Server starting",
zap.String("addr", f.Listen.IP), zap.String("addr", ip),
zap.Uint16("port", f.Listen.Port), zap.Uint16("port", port),
zap.String("default error page", opt.Default.PageCode), zap.String("default error page", opt.Default.PageCode),
zap.Uint16("default HTTP response code", opt.Default.HTTPCode), zap.Uint16("default HTTP response code", opt.Default.HTTPCode),
zap.Strings("proxy headers", opt.ProxyHTTPHeaders), zap.Strings("proxy headers", opt.ProxyHTTPHeaders),
@ -133,7 +242,7 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
zap.Bool("localization disabled", opt.L10n.Disabled), zap.Bool("localization disabled", opt.L10n.Disabled),
) )
if err := server.Start(f.Listen.IP, f.Listen.Port); err != nil { if err := server.Start(ip, port); err != nil {
errCh <- err errCh <- err
} }
}(startingErrCh) }(startingErrCh)

View File

@ -1,235 +1,220 @@
package serve package serve
import ( // import (
"fmt" // "fmt"
"net" // "net"
"sort" // "sort"
"strconv" // "strconv"
"strings" // "strings"
//
"github.com/spf13/pflag" // "github.com/spf13/pflag"
"github.com/tarampampam/error-pages/internal/env" //
"github.com/tarampampam/error-pages/internal/options" // "github.com/tarampampam/error-pages/internal/env"
) // "github.com/tarampampam/error-pages/internal/options"
// )
type flags struct { //
Listen struct { // type flags struct {
IP string // Listen struct {
Port uint16 // IP string
} // Port uint16
template struct { // }
name string // template struct {
} // name string
l10n struct { // }
disabled bool // l10n struct {
} // disabled bool
defaultErrorPage string // }
defaultHTTPCode uint16 // defaultErrorPage string
showDetails bool // defaultHTTPCode uint16
proxyHTTPHeaders string // comma-separated // showDetails bool
} // proxyHTTPHeaders string // comma-separated
// }
const ( //
listenFlagName = "listen" //
portFlagName = "port" //
templateNameFlagName = "template-name" // func (f *flags) Init(flagSet *pflag.FlagSet) {
defaultErrorPageFlagName = "default-error-page" // flagSet.StringVarP(
defaultHTTPCodeFlagName = "default-http-code" // &f.Listen.IP,
showDetailsFlagName = "show-details" // listenFlagName, "l",
proxyHTTPHeadersFlagName = "proxy-headers" // "0.0.0.0",
disableL10nFlagName = "disable-l10n" // fmt.Sprintf("IP address to Listen on [$%s]", env.ListenAddr),
) // )
// flagSet.Uint16VarP(
const ( // &f.Listen.Port,
useRandomTemplate = "random" // portFlagName, "p",
useRandomTemplateOnEachRequest = "i-said-random" // 8080, //nolint:gomnd // must be same as default healthcheck `--port` flag value
useRandomTemplateDaily = "random-daily" // fmt.Sprintf("TCP prt number [$%s]", env.ListenPort),
useRandomTemplateHourly = "random-hourly" // )
) // flagSet.StringVarP(
// &f.template.name,
func (f *flags) Init(flagSet *pflag.FlagSet) { // templateNameFlagName, "t",
flagSet.StringVarP( // "",
&f.Listen.IP, // fmt.Sprintf(
listenFlagName, "l", // "template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request "+
"0.0.0.0", // "or \"%s/%s\" daily/hourly randomized) [$%s]",
fmt.Sprintf("IP address to Listen on [$%s]", env.ListenAddr), // useRandomTemplate,
) // useRandomTemplateOnEachRequest,
flagSet.Uint16VarP( // useRandomTemplateDaily,
&f.Listen.Port, // useRandomTemplateHourly,
portFlagName, "p", // env.TemplateName,
8080, //nolint:gomnd // must be same as default healthcheck `--port` flag value // ),
fmt.Sprintf("TCP prt number [$%s]", env.ListenPort), // )
) // flagSet.StringVarP(
flagSet.StringVarP( // &f.defaultErrorPage,
&f.template.name, // defaultErrorPageFlagName, "",
templateNameFlagName, "t", // "404",
"", // fmt.Sprintf("default error page [$%s]", env.DefaultErrorPage),
fmt.Sprintf( // )
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on each request "+ // flagSet.Uint16VarP(
"or \"%s/%s\" daily/hourly randomized) [$%s]", // &f.defaultHTTPCode,
useRandomTemplate, // defaultHTTPCodeFlagName, "",
useRandomTemplateOnEachRequest, // 404, //nolint:gomnd
useRandomTemplateDaily, // fmt.Sprintf("default HTTP response code [$%s]", env.DefaultHTTPCode),
useRandomTemplateHourly, // )
env.TemplateName, // flagSet.BoolVarP(
), // &f.showDetails,
) // showDetailsFlagName, "",
flagSet.StringVarP( // false,
&f.defaultErrorPage, // fmt.Sprintf("show request details in response [$%s]", env.ShowDetails),
defaultErrorPageFlagName, "", // )
"404", // flagSet.StringVarP(
fmt.Sprintf("default error page [$%s]", env.DefaultErrorPage), // &f.proxyHTTPHeaders,
) // proxyHTTPHeadersFlagName, "",
flagSet.Uint16VarP( // "",
&f.defaultHTTPCode, // fmt.Sprintf("proxy HTTP request headers list (comma-separated) [$%s]", env.ProxyHTTPHeaders),
defaultHTTPCodeFlagName, "", // )
404, //nolint:gomnd // flagSet.BoolVarP(
fmt.Sprintf("default HTTP response code [$%s]", env.DefaultHTTPCode), // &f.l10n.disabled,
) // disableL10nFlagName, "",
flagSet.BoolVarP( // false,
&f.showDetails, // fmt.Sprintf("disable error pages localization [$%s]", env.DisableL10n),
showDetailsFlagName, "", // )
false, // }
fmt.Sprintf("show request details in response [$%s]", env.ShowDetails), //
) // func (f *flags) OverrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit,gocyclo
flagSet.StringVarP( // flagSet.VisitAll(func(flag *pflag.Flag) {
&f.proxyHTTPHeaders, // // flag was NOT defined using CLI (flags should have maximal priority)
proxyHTTPHeadersFlagName, "", // if !flag.Changed { //nolint:nestif
"", // switch flag.Name {
fmt.Sprintf("proxy HTTP request headers list (comma-separated) [$%s]", env.ProxyHTTPHeaders), // case listenFlagName:
) // if envVar, exists := env.ListenAddr.Lookup(); exists {
flagSet.BoolVarP( // f.Listen.IP = strings.TrimSpace(envVar)
&f.l10n.disabled, // }
disableL10nFlagName, "", //
false, // case portFlagName:
fmt.Sprintf("disable error pages localization [$%s]", env.DisableL10n), // if envVar, exists := env.ListenPort.Lookup(); exists {
) // if p, err := strconv.ParseUint(envVar, 10, 16); err == nil {
} // f.Listen.Port = uint16(p)
// } else {
func (f *flags) OverrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit,gocyclo // lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar)
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 templateNameFlagName:
case listenFlagName: // if envVar, exists := env.TemplateName.Lookup(); exists {
if envVar, exists := env.ListenAddr.Lookup(); exists { // f.template.name = strings.TrimSpace(envVar)
f.Listen.IP = strings.TrimSpace(envVar) // }
} //
// case defaultErrorPageFlagName:
case portFlagName: // if envVar, exists := env.DefaultErrorPage.Lookup(); exists {
if envVar, exists := env.ListenPort.Lookup(); exists { // f.defaultErrorPage = strings.TrimSpace(envVar)
if p, err := strconv.ParseUint(envVar, 10, 16); err == nil { //nolint:gomnd // }
f.Listen.Port = uint16(p) //
} else { // case defaultHTTPCodeFlagName:
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar) // if envVar, exists := env.DefaultHTTPCode.Lookup(); exists {
} // if code, err := strconv.ParseUint(envVar, 10, 16); err == nil {
} // f.defaultHTTPCode = uint16(code)
// } else {
case templateNameFlagName: // lastErr = fmt.Errorf("wrong default HTTP response code environment variable [%s] value", envVar)
if envVar, exists := env.TemplateName.Lookup(); exists { // }
f.template.name = strings.TrimSpace(envVar) // }
} //
// case showDetailsFlagName:
case defaultErrorPageFlagName: // if envVar, exists := env.ShowDetails.Lookup(); exists {
if envVar, exists := env.DefaultErrorPage.Lookup(); exists { // if b, err := strconv.ParseBool(envVar); err == nil {
f.defaultErrorPage = strings.TrimSpace(envVar) // f.showDetails = b
} // }
// }
case defaultHTTPCodeFlagName: //
if envVar, exists := env.DefaultHTTPCode.Lookup(); exists { // case proxyHTTPHeadersFlagName:
if code, err := strconv.ParseUint(envVar, 10, 16); err == nil { //nolint:gomnd // if envVar, exists := env.ProxyHTTPHeaders.Lookup(); exists {
f.defaultHTTPCode = uint16(code) // f.proxyHTTPHeaders = strings.TrimSpace(envVar)
} else { // }
lastErr = fmt.Errorf("wrong default HTTP response code environment variable [%s] value", envVar) //
} // case disableL10nFlagName:
} // if envVar, exists := env.DisableL10n.Lookup(); exists {
// if b, err := strconv.ParseBool(envVar); err == nil {
case showDetailsFlagName: // f.l10n.disabled = b
if envVar, exists := env.ShowDetails.Lookup(); exists { // }
if b, err := strconv.ParseBool(envVar); err == nil { // }
f.showDetails = b // }
} // }
} // })
//
case proxyHTTPHeadersFlagName: // return lastErr
if envVar, exists := env.ProxyHTTPHeaders.Lookup(); exists { // }
f.proxyHTTPHeaders = strings.TrimSpace(envVar) //
} // func (f *flags) Validate() error {
// if net.ParseIP(f.Listen.IP) == nil {
case disableL10nFlagName: // return fmt.Errorf("wrong IP address [%s] for listening", f.Listen.IP)
if envVar, exists := env.DisableL10n.Lookup(); exists { // }
if b, err := strconv.ParseBool(envVar); err == nil { //
f.l10n.disabled = b // if f.defaultHTTPCode > 599 { //nolint:gomnd
} // return fmt.Errorf("wrong default HTTP response code [%d]", f.defaultHTTPCode)
} // }
} //
} // if strings.ContainsRune(f.proxyHTTPHeaders, ' ') {
}) // return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", f.proxyHTTPHeaders)
// }
return lastErr //
} // return nil
// }
func (f *flags) Validate() error { //
if net.ParseIP(f.Listen.IP) == nil { // // headersToProxy converts a comma-separated string with headers list into strings slice (with a sorting and without
return fmt.Errorf("wrong IP address [%s] for listening", f.Listen.IP) // // duplicates).
} // func (f *flags) headersToProxy() []string {
// var raw = strings.Split(f.proxyHTTPHeaders, ",")
if f.defaultHTTPCode > 599 { //nolint:gomnd //
return fmt.Errorf("wrong default HTTP response code [%d]", f.defaultHTTPCode) // if len(raw) == 0 {
} // return []string{}
// } else if len(raw) == 1 {
if strings.ContainsRune(f.proxyHTTPHeaders, ' ') { // if h := strings.TrimSpace(raw[0]); h != "" {
return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", f.proxyHTTPHeaders) // return []string{h}
} // } else {
// return []string{}
return nil // }
} // }
//
// headersToProxy converts a comma-separated string with headers list into strings slice (with a sorting and without // var m = make(map[string]struct{}, len(raw))
// duplicates). //
func (f *flags) headersToProxy() []string { // // make unique and ignore empty strings
var raw = strings.Split(f.proxyHTTPHeaders, ",") // for _, h := range raw {
// if h = strings.TrimSpace(h); h != "" {
if len(raw) == 0 { // if _, ok := m[h]; !ok {
return []string{} // m[h] = struct{}{}
} else if len(raw) == 1 { // }
if h := strings.TrimSpace(raw[0]); h != "" { // }
return []string{h} // }
} else { //
return []string{} // // convert map into slice
} // var headers = make([]string, 0, len(m))
} // for h := range m {
// headers = append(headers, h)
var m = make(map[string]struct{}, len(raw)) // }
//
// make unique and ignore empty strings // // make sort
for _, h := range raw { // sort.Strings(headers)
if h = strings.TrimSpace(h); h != "" { //
if _, ok := m[h]; !ok { // return headers
m[h] = struct{}{} // }
} //
} // func (f *flags) ToOptions() (o options.ErrorPage) {
} // o.Default.PageCode = f.defaultErrorPage
// o.Default.HTTPCode = f.defaultHTTPCode
// convert map into slice // o.L10n.Disabled = f.l10n.disabled
var headers = make([]string, 0, len(m)) // o.Template.Name = f.template.name
for h := range m { // o.ShowDetails = f.showDetails
headers = append(headers, h) // o.ProxyHTTPHeaders = f.headersToProxy()
} //
// return o
// make sort // }
sort.Strings(headers)
return headers
}
func (f *flags) ToOptions() (o options.ErrorPage) {
o.Default.PageCode = f.defaultErrorPage
o.Default.HTTPCode = f.defaultHTTPCode
o.L10n.Disabled = f.l10n.disabled
o.Template.Name = f.template.name
o.ShowDetails = f.showDetails
o.ProxyHTTPHeaders = f.headersToProxy()
return o
}

View File

@ -0,0 +1,31 @@
package shared
import (
"github.com/urfave/cli/v2"
"github.com/tarampampam/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 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()},
}

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package config package config
import ( import (
"io/ioutil"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -68,7 +67,7 @@ func (t Template) Name() string { return t.name }
func (t Template) Content() []byte { return t.content } func (t Template) Content() []byte { return t.content }
func (t *Template) loadContentFromFile(filePath string) (err error) { func (t *Template) loadContentFromFile(filePath string) (err error) {
if t.content, err = ioutil.ReadFile(filePath); err != nil { 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 errors.Wrap(err, "cannot load content for the template "+t.Name()+" from file "+filePath)
} }
@ -236,7 +235,7 @@ func FromYaml(in []byte) (_ *Config, err error) {
// FromYamlFile creates new Config instance using YAML file. // FromYamlFile creates new Config instance using YAML file.
func FromYamlFile(filepath string) (*Config, error) { func FromYamlFile(filepath string) (*Config, error) {
bytes, err := ioutil.ReadFile(filepath) bytes, err := os.ReadFile(filepath)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot read configuration file") return nil, errors.Wrap(err, "cannot read configuration file")
} }

3
internal/env/env.go vendored
View File

@ -6,6 +6,9 @@ import "os"
type envVariable string type envVariable string
const ( const (
LogLevel envVariable = "LOG_LEVEL" // logging level
LogFormat envVariable = "LOG_FORMAT" // logging format (json|console)
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
ListenPort envVariable = "LISTEN_PORT" // port number for listening ListenPort envVariable = "LISTEN_PORT" // port number for listening
TemplateName envVariable = "TEMPLATE_NAME" // template name TemplateName envVariable = "TEMPLATE_NAME" // template name

View File

@ -39,7 +39,7 @@ func ClientWantFormat(ctx *fasthttp.RequestCtx) ContentType {
f := format{b[0:idx], 0} f := format{b[0:idx], 0}
if len(b) > idx+3 { if len(b) > idx+3 {
if weight, err := strconv.ParseFloat(string(b[idx+3:]), 32); err == nil { //nolint:gomnd if weight, err := strconv.ParseFloat(string(b[idx+3:]), 32); err == nil {
f.weight = float32(weight) f.weight = float32(weight)
} }
} }

68
internal/logger/format.go Normal file
View 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)
}

View File

@ -0,0 +1,62 @@
package logger_test
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/tarampampam/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
View 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)
}

View File

@ -0,0 +1,84 @@
package logger_test
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/tarampampam/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())
}

View File

@ -2,20 +2,27 @@
package logger package logger
import ( import (
"errors"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
// New creates new "zap" logger with little customization. // New creates new "zap" logger with a small customization.
func New(verbose, debug, logJSON bool) (*zap.Logger, error) { func New(l Level, f Format) (*zap.Logger, error) {
var config zap.Config var config zap.Config
if logJSON { switch f {
config = zap.NewProductionConfig() case ConsoleFormat:
} else {
config = zap.NewDevelopmentConfig() config = zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05") 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 // default configuration for all encoders
@ -24,15 +31,31 @@ func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
config.DisableStacktrace = true config.DisableStacktrace = true
config.DisableCaller = true config.DisableCaller = true
if debug { // enable additional features for debugging
if l <= DebugLevel {
config.Development = true config.Development = true
config.DisableStacktrace = false config.DisableStacktrace = false
config.DisableCaller = false config.DisableCaller = false
} }
if verbose || debug { var zapLvl zapcore.Level
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
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() return config.Build()
} }

View File

@ -8,64 +8,51 @@ import (
"github.com/kami-zh/go-capturer" "github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tarampampam/error-pages/internal/logger" "github.com/tarampampam/error-pages/internal/logger"
) )
func TestNewNotVerboseDebugJSON(t *testing.T) { func TestNewDebugLevelConsoleFormat(t *testing.T) {
output := capturer.CaptureStderr(func() { output := capturer.CaptureStderr(func() {
log, err := logger.New(false, false, false) log, err := logger.New(logger.DebugLevel, logger.ConsoleFormat)
assert.NoError(t, err) require.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg") log.Debug("dbg msg")
log.Info("inf msg")
log.Error("err msg") log.Error("err msg")
}) })
assert.Contains(t, output, time.Now().Format("15:04:05")) assert.Contains(t, output, time.Now().Format("15:04:05"))
assert.Regexp(t, `\t.+info.+\tinf msg`, output) 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.Regexp(t, `\t.+info.+\t.+logger_test\.go:\d+\tinf msg`, output)
assert.Contains(t, output, "dbg msg") assert.Contains(t, output, "dbg msg")
assert.Contains(t, output, "err msg") assert.Contains(t, output, "err msg")
} }
func TestNewNotVerboseDebugButJSON(t *testing.T) { func TestNewErrorLevelConsoleFormat(t *testing.T) {
output := capturer.CaptureStderr(func() { output := capturer.CaptureStderr(func() {
log, err := logger.New(false, false, true) log, err := logger.New(logger.ErrorLevel, logger.ConsoleFormat)
assert.NoError(t, err) require.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg 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") log.Error("err msg")
}) })
@ -75,6 +62,14 @@ func TestNewNotVerboseDebugButJSON(t *testing.T) {
lines := strings.Split(strings.Trim(output, "\n"), "\n") 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]) 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")
}

View File

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/tarampampam/error-pages/internal/version" "github.com/tarampampam/error-pages/internal/version"
) )
@ -30,6 +31,7 @@ var tplFnMap = template.FuncMap{ //nolint:gochecknoglobals
return 0 return 0
}, },
"env": os.Getenv,
} }
var ErrClosed = errors.New("closed") var ErrClosed = errors.New("closed")
@ -206,7 +208,7 @@ func (tr *TemplateRenderer) Close() error {
} }
func (tr *TemplateRenderer) mixHashes(a, b Hash) (result cacheEntryHash) { func (tr *TemplateRenderer) mixHashes(a, b Hash) (result cacheEntryHash) {
for i := 0; i < len(a); i++ { for i := 0; i < len(a); i++ { //nolint:gosimple
result[i] = a[i] result[i] = a[i]
} }

View File

@ -2,11 +2,14 @@ package tpl_test
import ( import (
"math/rand" "math/rand"
"os"
"strconv" "strconv"
"sync" "sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tarampampam/error-pages/internal/tpl" "github.com/tarampampam/error-pages/internal/tpl"
) )
@ -14,6 +17,10 @@ func Test_Render(t *testing.T) {
renderer := tpl.NewTemplateRenderer() renderer := tpl.NewTemplateRenderer()
defer func() { _ = renderer.Close() }() 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 { for name, tt := range map[string]struct {
giveContent string giveContent string
giveProps tpl.Properties giveProps tpl.Properties
@ -67,6 +74,11 @@ func Test_Render(t *testing.T) {
giveProps: tpl.Properties{L10nDisabled: true}, giveProps: tpl.Properties{L10nDisabled: true},
wantContent: "Y", wantContent: "Y",
}, },
"env": {
giveContent: `{{ env "TEST_ENV_VAR" }}`,
wantContent: "unit-test",
},
} { } {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
content, err := renderer.Render([]byte(tt.giveContent), tt.giveProps) content, err := renderer.Render([]byte(tt.giveContent), tt.giveProps)

View File

@ -7,301 +7,500 @@ Object.defineProperty(window, 'l10n', {
ru: 'Ошибка', ru: 'Ошибка',
uk: 'Помилка', uk: 'Помилка',
pt: 'Erro', pt: 'Erro',
nl: 'Fout',
de: 'Fehler',
es: 'Error',
zh: '错误',
}, },
'Good luck': { 'Good luck': {
fr: 'Bonne chance', fr: 'Bonne chance',
ru: 'Удачи', ru: 'Удачи',
uk: 'Успіхів', uk: 'Успіхів',
pt: 'Boa sorte', pt: 'Boa sorte',
nl: 'Veel succes',
de: 'Viel Glück',
es: 'Buena Suerte',
zh: '祝好运',
}, },
'UH OH': { 'UH OH': {
fr: 'Oups', fr: 'Oups',
ru: 'Ох', ru: 'Ох',
uk: 'Ох', uk: 'Ох',
pt: 'Ops', pt: 'Ops',
nl: 'Oeps',
de: 'Hoppla',
es: 'Oups',
zh: '哎呀',
}, },
'Request details': { 'Request details': {
fr: 'Détails de la requête', fr: 'Détails de la requête',
ru: 'Детали запроса', ru: 'Детали запроса',
uk: 'Деталі запиту', uk: 'Деталі запиту',
pt: 'Detalhes da solicitação', pt: 'Detalhes da solicitação',
nl: 'Details van verzoek',
de: 'Details der Anfrage',
es: 'Detalles de la petición',
zh: '请求详情',
}, },
'Double-check the URL': { 'Double-check the URL': {
fr: 'Vérifiez lURL', fr: 'Vérifiez lURL',
ru: 'Дважды проверьте URL', ru: 'Дважды проверьте URL',
uk: 'Двічі перевіряйте URL-адресу', uk: 'Двічі перевіряйте URL-адресу',
pt: 'Verifique novamente a URL', pt: 'Verifique novamente a URL',
nl: 'Controleer de URL',
de: 'Überprüfen Sie die URL',
es: 'Verifique la url',
zh: '请再次检查地址',
}, },
'Alternatively, go back': { 'Alternatively, go back': {
fr: 'Essayer de revenir en arrière', fr: 'Essayer de revenir en arrière',
ru: 'Или можете вернуться назад', ru: 'Или можете вернуться назад',
uk: 'Або ви можете повернутися', uk: 'Або ви можете повернутися',
pt: "Como alternativa, tente voltar", pt: "Como alternativa, tente voltar",
nl: 'Of ga terug',
de: 'Alternativ gehen Sie zurück',
es: 'Como alternativa, vuelva atrás',
zh: '或返回上一页',
}, },
'Here\'s what might have happened': { 'Here\'s what might have happened': {
fr: 'Voici ce qui aurait pu se passer', fr: 'Voici ce qui aurait pu se passer',
ru: 'Из-за чего это могло случиться', ru: 'Из-за чего это могло случиться',
uk: 'Що це може статися', uk: 'Що це може статися',
pt: 'Aqui está o que pode ter acontecido', 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: '可能原因有',
}, },
'You may have mistyped the URL': { 'You may have mistyped the URL': {
fr: 'Vous avez peut-être mal tapé lURL', fr: 'Vous avez peut-être mal tapé lURL',
ru: 'Вы могли ошибиться в URL', ru: 'Вы могли ошибиться в URL',
uk: 'Ви можете зробити помилку в URL-адресі', uk: 'Ви можете зробити помилку в URL-адресі',
pt: 'Você pode ter digitado incorretamente a 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: '您可能输入了错误的地址',
}, },
'The site was moved': { 'The site was moved': {
fr: 'Le site a été déplacé', fr: 'Le site a été déplacé',
ru: 'Сайт был перемещён', ru: 'Сайт был перемещён',
uk: 'Сайт був переміщений', uk: 'Сайт був переміщений',
pt: 'O site foi movido', pt: 'O site foi movido',
nl: 'De site is verplaatst',
de: 'Die Seite wurde verschoben',
es: 'El sitio se ha movido',
zh: '站点已被转移',
}, },
'It was never here': { 'It was never here': {
fr: 'Il na jamais été ici', fr: 'Il na jamais été ici',
ru: 'Он никогда не был здесь', ru: 'Он никогда не был здесь',
uk: 'Він ніколи не був тут', uk: 'Він ніколи не був тут',
pt: 'Nunca esteve aqui', pt: 'Nunca esteve aqui',
nl: 'Het was hier nooit',
de: 'Es war nie hier',
es: 'Nunca ha estado aquí',
zh: '站点从未存在',
}, },
'Bad Request': { 'Bad Request': {
fr: 'Mauvaise requête', fr: 'Mauvaise requête',
ru: 'Некорректный запрос', ru: 'Некорректный запрос',
uk: 'Неправильний запит', uk: 'Неправильний запит',
pt: 'Requisição inválida', pt: 'Requisição inválida',
nl: 'Foutieve anvraag',
de: 'Fehlerhafte Anfrage',
es: 'Petición inválida',
zh: '错误请求',
}, },
'The server did not understand the request': { 'The server did not understand the request': {
fr: 'Le serveur ne comprend pas la requête', fr: 'Le serveur ne comprend pas la requête',
ru: 'Сервер не смог обработать запрос из-за ошибки в нём', ru: 'Сервер не смог обработать запрос из-за ошибки в нём',
uk: 'Сервер не міг обробити запит через помилку в ньому', uk: 'Сервер не міг обробити запит через помилку в ньому',
pt: 'O servidor não entendeu a solicitação', 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: '服务器不理解该请求',
}, },
'Unauthorized': { 'Unauthorized': {
fr: 'Non autorisé', fr: 'Non autorisé',
ru: 'Запрос не авторизован', ru: 'Запрос не авторизован',
uk: 'Несанкціонований доступ', uk: 'Несанкціонований доступ',
pt: 'Não autorizado', pt: 'Não autorizado',
nl: 'Niet geautoriseerd',
de: 'Nicht autorisiert',
es: 'No autorizado',
zh: '未经授权',
}, },
'The requested page needs a username and a password': { 'The requested page needs a username and a password': {
fr: 'La page demandée nécessite un nom dutilisateur et un mot de passe', fr: 'La page demandée nécessite un nom dutilisateur et un mot de passe',
ru: 'Для доступа к странице требуется логин и пароль', ru: 'Для доступа к странице требуется логин и пароль',
uk: 'Щоб отримати доступ до сторінки, потрібний логін та пароль', uk: 'Щоб отримати доступ до сторінки, потрібний логін та пароль',
pt: 'A página solicitada precisa de um nome de usuário e uma senha', 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: '请求的页面需要用户名和密码',
}, },
'Forbidden': { 'Forbidden': {
fr: 'Interdit', fr: 'Interdit',
ru: 'Запрещено', ru: 'Запрещено',
uk: 'Заборонено', uk: 'Заборонено',
pt: 'Proibido', pt: 'Proibido',
nl: 'Verboden',
de: 'Verboten',
es: 'Prohibido',
zh: '禁止访问',
}, },
'Access is forbidden to the requested page': { 'Access is forbidden to the requested page': {
fr: 'Accès interdit à la page demandée', fr: 'Accès interdit à la page demandée',
ru: 'Доступ к странице запрещён', ru: 'Доступ к странице запрещён',
uk: 'Доступ до сторінки заборонено', uk: 'Доступ до сторінки заборонено',
pt: 'É proibido o acesso à página solicitada', 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: '禁止访问请求的页面',
}, },
'Not Found': { 'Not Found': {
fr: 'Introuvable', fr: 'Introuvable',
ru: 'Страница не найдена', ru: 'Страница не найдена',
uk: 'Сторінка не знайдена', uk: 'Сторінка не знайдена',
pt: 'Não encontrado', pt: 'Não encontrado',
nl: 'Niet gevonden',
de: 'Nicht gefunden',
es: 'No encontrado',
zh: '未找到',
}, },
'The server can not find the requested page': { 'The server can not find the requested page': {
fr: 'Le serveur ne peut trouver la page demandée', fr: 'Le serveur ne peut trouver la page demandée',
ru: 'Сервер не смог найти запрашиваемую страницу', ru: 'Сервер не смог найти запрашиваемую страницу',
uk: 'Сервер не міг знайти запитану сторінку', uk: 'Сервер не міг знайти запитану сторінку',
pt: 'O servidor não consegue encontrar a página solicitada', 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: '服务器找不到请求的页面',
}, },
'Method Not Allowed': { 'Method Not Allowed': {
fr: 'Méthode Non Autorisée', fr: 'Méthode Non Autorisée',
ru: 'Метод не поддерживается', ru: 'Метод не поддерживается',
uk: 'Неприпустимий метод', uk: 'Неприпустимий метод',
pt: 'Método não permitido', pt: 'Método não permitido',
nl: 'Methode niet toegestaan',
de: 'Methode nicht erlaubt',
es: 'Método no permitido',
zh: '方法不被允许',
}, },
'The method specified in the request is not allowed': { 'The method specified in the request is not allowed': {
fr: 'La méthode spécifiée dans la requête nest pas autorisée', fr: 'La méthode spécifiée dans la requête nest pas autorisée',
ru: 'Указанный в запросе метод не поддерживается', ru: 'Указанный в запросе метод не поддерживается',
uk: 'Метод, зазначений у запиті, не підтримується', uk: 'Метод, зазначений у запиті, не підтримується',
pt: 'O método especificado na solicitação não é permitido', 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: '请求指定的方法不被允许',
}, },
'Proxy Authentication Required': { 'Proxy Authentication Required': {
fr: 'Authentification proxy requise', fr: 'Authentification proxy requise',
ru: 'Нужна аутентификация прокси', ru: 'Нужна аутентификация прокси',
uk: 'Потрібна ідентифікація проксі', uk: 'Потрібна ідентифікація проксі',
pt: 'Autenticação de proxy necessária', 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: '需要代理服务器身份验证',
}, },
'You must authenticate with a proxy server before this request can be served': { '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', fr: 'Vous devez vous authentifier avec un serveur proxy avant que cette requête puisse être servie',
ru: 'Вы должны быть авторизованы на прокси сервере для обработки этого запроса', ru: 'Вы должны быть авторизованы на прокси сервере для обработки этого запроса',
uk: 'Ви повинні увійти до проксі-сервера для обробки цього запиту', uk: 'Ви повинні увійти до проксі-сервера для обробки цього запиту',
pt: 'Você deve se autenticar com um servidor proxy antes que esta solicitação possa ser atendida', 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: '您必须对代理服务器进行身份验证,然后才能让请求得到处理',
}, },
'Request Timeout': { 'Request Timeout': {
fr: 'Requête expiré', fr: 'Requête expiré',
ru: 'Истекло время ожидания', ru: 'Истекло время ожидания',
uk: 'Час запиту закінчився', uk: 'Час запиту закінчився',
pt: 'Tempo limite de solicitação excedido', 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: '请求超时',
}, },
'The request took longer than the server was prepared to wait': { 'The request took longer than the server was prepared to wait': {
fr: 'La requête prend plus de temps que prévu', fr: 'La requête prend plus de temps que prévu',
ru: 'Отправка запроса заняла слишком много времени', ru: 'Отправка запроса заняла слишком много времени',
uk: 'Надсилання запиту зайняв занадто багато часу', uk: 'Надсилання запиту зайняв занадто багато часу',
pt: 'A solicitação demorou mais do que o servidor estava preparado para esperar', 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: '请求用时超过了服务器设置的最长等待时间',
}, },
'Conflict': { 'Conflict': {
fr: 'Conflit', fr: 'Conflit',
ru: 'Конфликт', ru: 'Конфликт',
uk: 'Конфлікт', uk: 'Конфлікт',
pt: 'Conflito', pt: 'Conflito',
nl: 'Conflict',
de: 'Konflikt',
es: 'Conflicto',
zh: '冲突',
}, },
'The request could not be completed because of a conflict': { 'The request could not be completed because of a conflict': {
fr: 'La requête na pas pu être complétée à cause dun conflit', fr: 'La requête na pas pu être complétée à cause dun conflit',
ru: 'Запрос не может быть обработан из-за конфликта', ru: 'Запрос не может быть обработан из-за конфликта',
uk: 'Запит не може бути оброблений через конфлікт', uk: 'Запит не може бути оброблений через конфлікт',
pt: 'A solicitação não pôde ser concluída devido a um conflito', 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: '由于冲突,请求无法完成',
}, },
'Gone': { 'Gone': {
fr: 'Supprimé', fr: 'Supprimé',
ru: 'Удалено', ru: 'Удалено',
uk: 'Вилучений', uk: 'Вилучений',
pt: 'Removido', pt: 'Removido',
nl: 'Verdwenen',
de: 'Verschwunden',
es: 'Eliminado',
zh: '已移除',
}, },
'The requested page is no longer available': { 'The requested page is no longer available': {
fr: 'La page demandée nest plus disponible', fr: 'La page demandée nest plus disponible',
ru: 'Запрошенная страница была удалена', ru: 'Запрошенная страница была удалена',
uk: 'Запитана сторінка була видалена', uk: 'Запитана сторінка була видалена',
pt: 'A página solicitada não está mais disponível', 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: '请求的页面不再可用',
}, },
'Length Required': { 'Length Required': {
fr: 'Longueur requise', fr: 'Longueur requise',
ru: 'Необходима длина', ru: 'Необходима длина',
uk: 'Потрібно вказати розмір', uk: 'Потрібно вказати розмір',
pt: 'Content-Length necessário', pt: 'Content-Length necessário',
nl: 'Lengte benodigd',
de: 'Länge benötigt',
es: 'Longitud requerida',
zh: '需要长度',
}, },
'The "Content-Length" is not defined. The server will not accept the request without it': { 'The "Content-Length" is not defined. The server will not accept the request without it': {
fr: 'Le "Content-Length" nest pas défini. Le serveur ne prendra pas en compte la requête', fr: 'Le "Content-Length" nest pas défini. Le serveur ne prendra pas en compte la requête',
ru: 'Заголовок "Content-Length" не был передан. Сервер не может обработать запрос без него', ru: 'Заголовок "Content-Length" не был передан. Сервер не может обработать запрос без него',
uk: 'Заголовок "Content-Length" не був переданий. Сервер не може обробити запит без нього', uk: 'Заголовок "Content-Length" не був переданий. Сервер не може обробити запит без нього',
pt: 'O "Content-Length" não está definido. O servidor não aceitará a solicitação sem ele', 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(内容长度)。服务器将不接受不包含此头信息的请求',
}, },
'Precondition Failed': { 'Precondition Failed': {
fr: 'Échec de la condition préalable', fr: 'Échec de la condition préalable',
ru: 'Условие ложно', ru: 'Условие ложно',
uk: 'Збій під час обробки попередньої умови', uk: 'Збій під час обробки попередньої умови',
pt: 'Falha na pré-condição', 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': { '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', fr: 'La précondition donnée dans la requête a été évaluée comme étant fausse par le serveur',
ru: 'Ни одно из условных полей заголовка запроса не было выполнено', ru: 'Ни одно из условных полей заголовка запроса не было выполнено',
uk: 'Жодна з умовних полів заголовка запиту не була виконана', uk: 'Жодна з умовних полів заголовка запиту не була виконана',
pt: 'A pré-condição dada na solicitação avaliada como falsa pelo servidor', 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(假)',
}, },
'Payload Too Large': { 'Payload Too Large': {
fr: 'Charge trop volumineuse', fr: 'Charge trop volumineuse',
ru: 'Слишком большой запрос', ru: 'Слишком большой запрос',
uk: 'Занадто великий запит', uk: 'Занадто великий запит',
pt: 'Payload muito grande', pt: 'Payload muito grande',
nl: 'Aanvraag te grood',
de: 'Anfrage zu groß',
es: 'Carga muy grande',
zh: '请求体过大',
}, },
'The server will not accept the request, because the request entity is too large': { '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 lentité de la requête est trop volumineuse', fr: 'Le serveur ne prendra pas en compte la requête, car lentité de la requête est trop volumineuse',
ru: 'Сервер не может обработать запрос, так как он слишком большой', ru: 'Сервер не может обработать запрос, так как он слишком большой',
uk: 'Сервер не може обробити запит, оскільки він занадто великий', uk: 'Сервер не може обробити запит, оскільки він занадто великий',
pt: 'O servidor não aceitará a solicitação porque a entidade da solicitação é muito grande', 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: '请求体过大,服务器将不接受该请求',
}, },
'Requested Range Not Satisfiable': { 'Requested Range Not Satisfiable': {
fr: 'Requête non satisfaisante', fr: 'Requête non satisfaisante',
ru: 'Диапазон не достижим', ru: 'Диапазон не достижим',
uk: 'Запитуваний діапазон недосяжний', uk: 'Запитуваний діапазон недосяжний',
pt: 'Intervalo Solicitado Não Satisfatório', pt: 'Intervalo Solicitado Não Satisfatório',
nl: 'Aangevraagd gedeelte niet opvraagbaar',
de: 'Anfrage-Bereich nicht erfüllbar',
es: 'Intervalo solicitado no satisfactorio',
zh: '不满足请求范围',
}, },
'The requested byte range is not available and is out of bounds': { 'The requested byte range is not available and is out of bounds': {
fr: 'Le byte range demandé nest pas disponible et est hors des limites', fr: 'Le byte range demandé nest pas disponible et est hors des limites',
ru: 'Запрошенный диапазон данных недоступен или вне допустимых пределов', ru: 'Запрошенный диапазон данных недоступен или вне допустимых пределов',
uk: 'Описаний діапазон даних недоступний або з допустимих меж', uk: 'Описаний діапазон даних недоступний або з допустимих меж',
pt: 'O intervalo de bytes solicitado não está disponível e está fora dos limites', 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: '请求的字节范围不可用,超出边界',
}, },
'I\'m a teapot': { 'I\'m a teapot': {
fr: 'Je suis une théière', fr: 'Je suis une théière',
ru: 'Я чайник', ru: 'Я чайник',
uk: 'Я чайник', uk: 'Я чайник',
pt: 'Eu sou um bule', pt: 'Eu sou um bule',
nl: 'Ik ben een theepot',
de: 'Ich bin eine Teekanne',
es: 'Soy una tetera',
zh: '我是一只茶壶',
}, },
'Attempt to brew coffee with a teapot is not supported': { 'Attempt to brew coffee with a teapot is not supported': {
fr: 'Tenter de préparer du café avec une théière nest pas pris en charge', fr: 'Tenter de préparer du café avec une théière nest pas pris en charge',
ru: 'Попытка заварить кофе в чайнике обречена на фиаско', ru: 'Попытка заварить кофе в чайнике обречена на фиаско',
uk: 'Спроба виварити каву в чайник приречена на фіаско', uk: 'Спроба виварити каву в чайник приречена на фіаско',
pt: 'A tentativa de preparar café com um bule não é suportada', 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: '用茶壶泡咖啡不受支持',
}, },
'Too Many Requests': { 'Too Many Requests': {
fr: 'Trop de requêtes', fr: 'Trop de requêtes',
ru: 'Слишком много запросов', ru: 'Слишком много запросов',
uk: 'Занадто багато запитів', uk: 'Занадто багато запитів',
pt: 'Excesso de solicitações', pt: 'Excesso de solicitações',
nl: 'Te veel requests',
de: 'Zu viele Anfragen',
es: 'Demasiadas peticiones',
zh: '请求过多',
}, },
'Too many requests in a given amount of time': { 'Too many requests in a given amount of time': {
fr: 'Trop de requêtes dans un délai donné', fr: 'Trop de requêtes dans un délai donné',
ru: 'Отправлено слишком много запросов за короткое время', ru: 'Отправлено слишком много запросов за короткое время',
uk: 'Надіслано занадто багато запитів на короткий час', uk: 'Надіслано занадто багато запитів на короткий час',
pt: 'Excesso de solicitações em um determinado período de tempo', 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: '在给定的时间内发送了过多请求',
}, },
'Internal Server Error': { 'Internal Server Error': {
fr: 'Erreur interne du serveur', fr: 'Erreur interne du serveur',
ru: 'Внутренняя ошибка сервера', ru: 'Внутренняя ошибка сервера',
uk: 'Внутрішня помилка сервера', uk: 'Внутрішня помилка сервера',
pt: 'Erro do Servidor Interno', pt: 'Erro do Servidor Interno',
nl: 'Interne serverfout',
de: 'Interner Server-Fehler',
es: 'Error Interno',
zh: '内部服务器错误',
}, },
'The server met an unexpected condition': { 'The server met an unexpected condition': {
fr: 'Le serveur a rencontré une condition inattendue', fr: 'Le serveur a rencontré une condition inattendue',
ru: 'Произошло что-то неожиданное на сервере', ru: 'Произошло что-то неожиданное на сервере',
uk: 'На сервері було щось несподіване', uk: 'На сервері було щось несподіване',
pt: 'O servidor encontrou uma condição inesperada', 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: '服务器遇到了意外情况',
}, },
'Bad Gateway': { 'Bad Gateway': {
fr: 'Mauvaise passerelle', fr: 'Mauvaise passerelle',
ru: 'Ошибка шлюза', ru: 'Ошибка шлюза',
uk: 'Помилка шлюзу', uk: 'Помилка шлюзу',
pt: 'Gateway inválido', pt: 'Gateway inválido',
nl: 'Ongeldige Gateway',
de: 'Fehlerhafter Gateway',
es: 'Puerta de enlace no valida',
zh: '无效网关',
}, },
'The server received an invalid response from the upstream server': { 'The server received an invalid response from the upstream server': {
fr: 'Le serveur a reçu une réponse invalide du serveur distant', fr: 'Le serveur a reçu une réponse invalide du serveur distant',
ru: 'Сервер получил некорректный ответ от вышестоящего сервера', ru: 'Сервер получил некорректный ответ от вышестоящего сервера',
uk: 'Сервер отримав неправильну відповідь з сервера Upstream', uk: 'Сервер отримав неправильну відповідь з сервера Upstream',
pt: 'O servidor recebeu uma resposta inválida do servidor upstream', pt: 'O servidor recebeu uma resposta inválida do servidor upstream',
nl: 'De server ontving een ongeldig antwoord van een bovenliggende server',
de: 'Der Server hat eine ungültige Antwort vom Upstream-Server erhalten',
es: 'El servidor ha recibido una respuesta no válida del servidor de origen',
zh: '服务器从上游服务器收到了无效的响应',
}, },
'Service Unavailable': { 'Service Unavailable': {
fr: 'Service indisponible', fr: 'Service indisponible',
ru: 'Сервис недоступен', ru: 'Сервис недоступен',
uk: 'Сервіс недоступний', uk: 'Сервіс недоступний',
pt: 'Serviço não disponível', pt: 'Serviço não disponível',
nl: 'Dienst niet beschikbaar',
de: 'Dienst nicht verfügbar',
es: 'Servicio no disponible',
zh: '服务不可用',
}, },
'The server is temporarily overloading or down': { 'The server is temporarily overloading or down': {
fr: 'Le serveur est temporairement en surcharge ou indisponible', fr: 'Le serveur est temporairement en surcharge ou indisponible',
ru: 'Сервер временно не может обрабатывать запросы по техническим причинам', ru: 'Сервер временно не может обрабатывать запросы по техническим причинам',
uk: 'Сервер тимчасово не може обробляти запити з технічних причин', uk: 'Сервер тимчасово не може обробляти запити з технічних причин',
pt: 'O servidor está temporariamente sobrecarregado ou inativo', 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: '服务器暂时过载或不可用',
}, },
'Gateway Timeout': { 'Gateway Timeout': {
fr: 'Expiration Passerelle', fr: 'Expiration Passerelle',
ru: 'Шлюз не отвечает', ru: 'Шлюз не отвечает',
uk: 'Шлюз не відповідає', uk: 'Шлюз не відповідає',
pt: 'Tempo limite do gateway excedido', pt: 'Tempo limite do gateway excedido',
nl: 'Gateway Verlopen',
de: 'Gateway Zeitüberschreitung',
es: 'Tiempo límite de puerta de enlace excedido',
zh: '网关超时',
}, },
'The gateway has timed out': { 'The gateway has timed out': {
fr: 'Le temps dattente de la passerelle est dépassé', fr: 'Le temps dattente de la passerelle est dépassé',
ru: 'Сервер не дождался ответа от вышестоящего сервера', ru: 'Сервер не дождался ответа от вышестоящего сервера',
uk: 'Сервер не чекав відповіді від сервера Upstream', uk: 'Сервер не чекав відповіді від сервера Upstream',
pt: 'O gateway esgotou o tempo limite', 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: '网关响应已经超时',
}, },
'HTTP Version Not Supported': { 'HTTP Version Not Supported': {
fr: 'Version HTTP non prise en charge', fr: 'Version HTTP non prise en charge',
ru: 'Версия HTTP не поддерживается', ru: 'Версия HTTP не поддерживается',
uk: 'Версія НТТР не підтримується', uk: 'Версія НТТР не підтримується',
pt: 'Versão HTTP não suportada', 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版本不受支持',
}, },
'The server does not support the "http protocol" version': { 'The server does not support the "http protocol" version': {
fr: 'Le serveur ne supporte pas la version du protocole HTTP', fr: 'Le serveur ne supporte pas la version du protocole HTTP',
ru: 'Сервер не поддерживает запрошенную версию HTTP протокола', ru: 'Сервер не поддерживает запрошенную версию HTTP протокола',
uk: 'Сервер не підтримує запитану версію HTTP-протоколу', uk: 'Сервер не підтримує запитану версію HTTP-протоколу',
pt: 'O servidor não suporta a versão do protocolo 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协议版本',
}, },
'Host': { 'Host': {
@ -309,54 +508,90 @@ Object.defineProperty(window, 'l10n', {
ru: 'Хост', ru: 'Хост',
uk: 'Хост', uk: 'Хост',
pt: 'Hospedeiro', pt: 'Hospedeiro',
nl: 'Host',
de: 'Host',
es: 'Host',
zh: '主机',
}, },
'Original URI': { 'Original URI': {
fr: 'URI dorigine', fr: 'URI dorigine',
ru: 'Исходный URI', ru: 'Исходный URI',
uk: 'Вихідний URI', uk: 'Вихідний URI',
pt: 'URI original', pt: 'URI original',
nl: 'Originele URI',
de: 'Originale URI',
es: 'URI original',
zh: '原始URI',
}, },
'Forwarded for': { 'Forwarded for': {
fr: 'Transmis pour', fr: 'Transmis pour',
ru: 'Перенаправлен', ru: 'Перенаправлен',
uk: 'Перенаправлений', uk: 'Перенаправлений',
pt: 'Encaminhado para', pt: 'Encaminhado para',
nl: 'Doorgestuurd voor',
de: 'Weitergeleitet für',
es: 'Remitido por',
zh: '转发自',
}, },
'Namespace': { 'Namespace': {
fr: 'Espace de noms', fr: 'Espace de noms',
ru: 'Пространство имён', ru: 'Пространство имён',
uk: 'Простір імен', uk: 'Простір імен',
pt: 'Namespace', pt: 'Namespace',
nl: 'Elementnaam',
de: 'Namensraum',
es: 'Namespace',
zh: '命名空间',
}, },
'Ingress name': { 'Ingress name': {
fr: 'Nom ingress', fr: 'Nom ingress',
ru: 'Имя Ingress', ru: 'Имя Ingress',
uk: 'Ім\'я Ingress', uk: 'Ім\'я Ingress',
pt: 'Nome Ingress', pt: 'Nome Ingress',
nl: 'Ingress naam',
de: 'Ingress Name',
es: 'Nombre Ingress',
zh: '入口名',
}, },
'Service name': { 'Service name': {
fr: 'Nom du service', fr: 'Nom du service',
ru: 'Имя сервиса', ru: 'Имя сервиса',
uk: 'Ім\'я сервісу', uk: 'Ім\'я сервісу',
pt: 'Nome do Serviço', pt: 'Nome do Serviço',
nl: 'Service naam',
de: 'Service Name',
es: 'Nombre del servicio',
zh: '服务名',
}, },
'Service port': { 'Service port': {
fr: 'Port du service', fr: 'Port du service',
ru: 'Порт сервиса', ru: 'Порт сервиса',
uk: 'Порт сервісу', uk: 'Порт сервісу',
pt: 'Porta do serviço', pt: 'Porta do serviço',
nl: 'Service poort',
de: 'Service Port',
es: 'Puerto del servicio',
zh: '服务端口',
}, },
'Request ID': { 'Request ID': {
fr: 'Identifiant de la requête', fr: 'Identifiant de la requête',
ru: 'ID запроса', ru: 'ID запроса',
uk: 'ID запиту', uk: 'ID запиту',
pt: 'ID da solicitação', pt: 'ID da solicitação',
nl: 'ID van het verzoek',
de: 'Anfrage ID',
es: 'ID de la petición',
zh: '请求ID',
}, },
'Timestamp': { 'Timestamp': {
fr: 'Horodatage', fr: 'Horodatage',
ru: 'Временная метка', ru: 'Временная метка',
uk: 'Тимчасова мітка', uk: 'Тимчасова мітка',
pt: 'Timestamp', pt: 'Timestamp',
nl: 'Tijdstempel',
de: 'Zeitstempel',
es: 'Timestamp',
zh: '时间戳',
}, },
'client-side error': { 'client-side error': {
@ -364,12 +599,20 @@ Object.defineProperty(window, 'l10n', {
ru: 'ошибка на стороне клиента', ru: 'ошибка на стороне клиента',
uk: 'помилка на стороні клієнта', uk: 'помилка на стороні клієнта',
pt: 'erro do lado do cliente', pt: 'erro do lado do cliente',
nl: 'fout aan de gebruikerskant',
de: 'Clientseitiger Fehler',
es: 'Error del lado del cliente',
zh: '客户端错误',
}, },
'server-side error': { 'server-side error': {
fr: 'Erreur Serveur', fr: 'Erreur Serveur',
ru: 'ошибка на стороне сервера', ru: 'ошибка на стороне сервера',
uk: 'помилка на стороні сервера', uk: 'помилка на стороні сервера',
pt: 'erro do lado do servidor', pt: 'erro do lado do servidor',
nl: 'fout aan de serverkant',
de: 'Serverseitiger Fehler',
es: 'Error del lado del servidor',
zh: '服务端错误',
}, },
'Your Client': { 'Your Client': {
@ -377,66 +620,110 @@ Object.defineProperty(window, 'l10n', {
ru: 'Ваш Браузер', ru: 'Ваш Браузер',
uk: 'Ваш Браузер', uk: 'Ваш Браузер',
pt: 'Seu Cliente', pt: 'Seu Cliente',
nl: 'Jouw Client',
de: 'Ihr Client',
es: 'Tu cliente',
zh: '您的客户端',
}, },
'Network': { 'Network': {
fr: 'Réseau', fr: 'Réseau',
ru: 'Сеть', ru: 'Сеть',
uk: 'Сіть', uk: 'Сіть',
pt: 'Rede', pt: 'Rede',
nl: 'Netwerk',
de: 'Netzwerk',
es: 'Red',
zh: '网络',
}, },
'Web Server': { 'Web Server': {
fr: 'Serveur Web', fr: 'Serveur Web',
ru: 'Web Сервер', ru: 'Web Сервер',
uk: 'Web Сервер', uk: 'Web Сервер',
pt: 'Servidor web', pt: 'Servidor web',
nl: 'Web Server',
de: 'Webserver',
es: 'Servidor web',
zh: 'Web服务器',
}, },
'What happened?': { 'What happened?': {
fr: 'Que sest-il passé ?', fr: 'Que sest-il passé ?',
ru: 'Что произошло?', ru: 'Что произошло?',
uk: 'Що сталося?', uk: 'Що сталося?',
pt: 'O que aconteceu?', pt: 'O que aconteceu?',
nl: 'Wat is er gebeurd?',
de: 'Was ist passiert?',
es: '¿Que ha pasado?',
zh: '发生了什么?',
}, },
'What can i do?': { 'What can i do?': {
fr: 'Que puis-je faire ?', fr: 'Que puis-je faire ?',
ru: 'Что можно сделать?', ru: 'Что можно сделать?',
uk: 'Що можна зробити?', uk: 'Що можна зробити?',
pt: 'O que eu posso fazer?', pt: 'O que eu posso fazer?',
nl: 'Wat kan ik doen?',
de: 'Was kann ich machen?',
es: '¿Que puedo hacer?',
zh: '我能做什么?',
}, },
'Please try again in a few minutes': { 'Please try again in a few minutes': {
fr: 'Veuillez réessayer dans quelques minutes', fr: 'Veuillez réessayer dans quelques minutes',
ru: 'Пожалуйста, попробуйте повторить запрос ещё раз чуть позже', ru: 'Пожалуйста, попробуйте повторить запрос ещё раз чуть позже',
uk: 'Будь ласка, спробуйте повторити запит ще раз трохи пізніше', uk: 'Будь ласка, спробуйте повторити запит ще раз трохи пізніше',
pt: 'Por favor, tente novamente em alguns minutos', 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: '请在几分钟后重试',
}, },
'Working': { 'Working': {
fr: 'Opérationnel', fr: 'Opérationnel',
ru: 'Работает', ru: 'Работает',
uk: 'Працює', uk: 'Працює',
pt: 'Funcionando', pt: 'Funcionando',
nl: 'Functioneel',
de: 'Funktioniert',
es: 'Trabajando',
zh: '正常运行',
}, },
'Unknown': { 'Unknown': {
fr: 'Inconnu', fr: 'Inconnu',
ru: 'Неизвестно', ru: 'Неизвестно',
uk: 'Невідомо', uk: 'Невідомо',
pt: 'Desconhecido', pt: 'Desconhecido',
nl: 'Onbekend',
de: 'Unbekannt',
es: 'Desconocido',
zh: '未知',
}, },
'Please try to change the request method, headers, payload, or URL': { '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 lURL', fr: 'Veuillez essayer de changer la méthode de requête, les en-têtes, le contenu ou lURL',
ru: 'Пожалуйста, попробуйте изменить метод запроса, заголовки, его содержимое или URL', ru: 'Пожалуйста, попробуйте изменить метод запроса, заголовки, его содержимое или URL',
uk: 'Будь ласка, спробуйте змінити метод запиту, заголовки, його вміст або URL-адресу', uk: 'Будь ласка, спробуйте змінити метод запиту, заголовки, його вміст або URL-адресу',
pt: 'Tente alterar o método de solicitação, cabeçalhos, payload ou 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',
}, },
'Please check your authorization data': { 'Please check your authorization data': {
fr: 'Veuillez vérifier vos données dautorisation', fr: 'Veuillez vérifier vos données dautorisation',
ru: 'Пожалуйста, проверьте данные авторизации', ru: 'Пожалуйста, проверьте данные авторизации',
uk: 'Будь ласка, перевірте дані авторизації', uk: 'Будь ласка, перевірте дані авторизації',
pt: 'Verifique seus dados de autorização', 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: '请检查您的授权数据',
}, },
'Please double-check the URL and try again': { 'Please double-check the URL and try again': {
fr: 'Veuillez vérifier lURL et réessayer', fr: 'Veuillez vérifier lURL et réessayer',
ru: 'Пожалуйста, дважды проверьте URL и попробуйте снова', ru: 'Пожалуйста, дважды проверьте URL и попробуйте снова',
uk: 'Будь ласка, двічі перевірте URL-адресу і спробуйте знову', uk: 'Будь ласка, двічі перевірте URL-адресу і спробуйте знову',
pt: 'Verifique novamente o URL e tente novamente', 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并重试',
}, },
}; };

View File

@ -14,3 +14,8 @@ By default, pages markup contains strings in English (`en` locale). If you want
## 👍 Translators ## 👍 Translators
- 🇫🇷 French by [@jvin042](https://github.com/jvin042) - 🇫🇷 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)

View File

@ -10,9 +10,9 @@
<meta name="robots" content="noindex, nofollow"/> <meta name="robots" content="noindex, nofollow"/>
<title>{{ message }}</title> <title>{{ message }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.bunny.net/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<style> <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} :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){ @media (prefers-color-scheme:dark){

View File

@ -10,8 +10,9 @@
<meta name="robots" content="noindex, nofollow"/> <meta name="robots" content="noindex, nofollow"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{{ code }} | {{ message }}</title> <title>{{ code }} | {{ message }}</title>
<link rel="preconnect" href="https://fonts.gstatic.com"/> <link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Red+Hat+Display:wght@500&family=Fira+Mono&family=Ubuntu&display=swap" rel="stylesheet"/> <link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Red+Hat+Display:wght@500&family=Fira+Mono&family=Ubuntu&display=swap" rel="stylesheet"/>
<style> <style>
/** Idea author: https://github.com/186526/CloudflareCustomErrorPage */ /** Idea author: https://github.com/186526/CloudflareCustomErrorPage */
:root{--color-bg-primary:#fff;--color-text-primary:#000;--color-text-secondary:#575958;--font-size-primary:56px;--font-size-secondary:20px;--ui-card-color-bg:#f2f2f2;--color-text-ok:#137333;--color-bg-ok:#e6f4ea;--color-text-error:#c5221f;--color-bg-error:#fce8e6;--color-text-warning:#b05a00;--color-bg-warning:#fef7e0;--icon-size:48px} :root{--color-bg-primary:#fff;--color-text-primary:#000;--color-text-secondary:#575958;--font-size-primary:56px;--font-size-secondary:20px;--ui-card-color-bg:#f2f2f2;--color-text-ok:#137333;--color-bg-ok:#e6f4ea;--color-text-error:#c5221f;--color-bg-error:#fce8e6;--color-text-warning:#b05a00;--color-bg-warning:#fef7e0;--icon-size:48px}

View File

@ -9,8 +9,9 @@
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<title>{{ code }}: {{ message }}</title> <title>{{ code }}: {{ message }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet"> <link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Open+Sans:wght@400;700&display=swap" rel="stylesheet">
<style> <style>
html,body {background-color:#1a1a1a;color:#fff;font-family:'Open Sans',sans-serif;height:100vh;margin:0;font-size:0} html,body {background-color:#1a1a1a;color:#fff;font-family:'Open Sans',sans-serif;height:100vh;margin:0;font-size:0}
.container {height:100vh;align-items:center;display:flex;justify-content:center;position:relative} .container {height:100vh;align-items:center;display:flex;justify-content:center;position:relative}

View File

@ -9,8 +9,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<title>{{ message }}</title> <title>{{ message }}</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet"> <link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
<style> <style>
/** Idea author: https://codepen.io/robinselmer */ /** Idea author: https://codepen.io/robinselmer */
html, body { html, body {

View File

@ -9,8 +9,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<title>{{ message }}</title> <title>{{ message }}</title>
<link rel="dns-prefetch" href="//fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> <link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<style> <style>
html,body {background-color:#222526;color:#fff;font-family:'Nunito',sans-serif;font-weight:100;height:100vh;margin:0;font-size:0} html,body {background-color:#222526;color:#fff;font-family:'Nunito',sans-serif;font-weight:100;height:100vh;margin:0;font-size:0}
.full-height {height:100vh} .full-height {height:100vh}

View File

@ -9,8 +9,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" /> <meta name="robots" content="noindex, nofollow" />
<title>{{ message }}</title> <title>{{ message }}</title>
<link rel="dns-prefetch" href="//fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> <link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<style> <style>
:root {--color-bg-primary:#fff;--color-text-primary:#636b6f;--color-text-secondary:#777} :root {--color-bg-primary:#fff;--color-text-primary:#636b6f;--color-text-secondary:#777}
@media (prefers-color-scheme: dark) {:root {--color-bg-primary:#222526;--color-text-primary:#fff;--color-text-secondary:#999}} @media (prefers-color-scheme: dark) {:root {--color-bg-primary:#222526;--color-text-primary:#fff;--color-text-secondary:#999}}

View File

@ -10,8 +10,9 @@
<meta name="robots" content="noindex, nofollow"/> <meta name="robots" content="noindex, nofollow"/>
<title>{{ message }}</title> <title>{{ message }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="dns-prefetch" href="//fonts.gstatic.com"> <link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link href="https://fonts.googleapis.com/css?family=Nunito+Sans" rel="stylesheet"> <link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito+Sans" rel="stylesheet">
<style> <style>
/** Codepen: https://codepen.io/kdbkapsere/pen/oNXLbqQ */ /** Codepen: https://codepen.io/kdbkapsere/pen/oNXLbqQ */

View File

@ -10,8 +10,9 @@
<meta name="robots" content="noindex, nofollow"/> <meta name="robots" content="noindex, nofollow"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ message }} ({{ code }})</title> <title>{{ message }} ({{ code }})</title>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet"> <link rel="dns-prefetch" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css2?family=Inconsolata:wght@400;700&display=swap" rel="stylesheet">
<style> <style>
:root{--matrix-glyph-size:15px;--matrix-glyph-font-size:15px;--matrix-glyph-front-color:rgba(255, 255, 255, 0.8);--matrix-glyph-tail-color:#0f0;--matrix-overlay-color:rgba(0, 0, 0, 0.12)} :root{--matrix-glyph-size:15px;--matrix-glyph-font-size:15px;--matrix-glyph-front-color:rgba(255, 255, 255, 0.8);--matrix-glyph-tail-color:#0f0;--matrix-overlay-color:rgba(0, 0, 0, 0.12)}
body,html{margin:0;padding:0;background-color:#000;height:100vh} body,html{margin:0;padding:0;background-color:#000;height:100vh}