Compare commits

..

30 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
39 changed files with 1211 additions and 802 deletions

View File

@ -27,7 +27,7 @@ jobs:
arch: [amd64] # amd64, 386
steps:
- uses: actions/setup-go@v3
with: {go-version: 1.18}
with: {go-version: 1.19}
- uses: actions/checkout@v3

View File

@ -29,7 +29,11 @@ jobs: # Docs: <https://git.io/JvxXE>
steps:
- uses: actions/checkout@v3
- uses: golangci/golangci-lint-action@v3 # Action page: <https://github.com/golangci/golangci-lint-action>
- uses: gacts/setup-go-with-cache@v1
with: {go-version: 1.19}
- uses: golangci/golangci-lint-action@v3
with: {skip-pkg-cache: true, skip-build-cache: true}
validate-config-file:
name: Validate config file
@ -67,7 +71,7 @@ jobs: # Docs: <https://git.io/JvxXE>
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v3
with: {go-version: 1.18}
with: {go-version: 1.19}
- uses: actions/checkout@v3
with: {fetch-depth: 2} # Fixes codecov error 'Issue detecting commit SHA'
@ -103,7 +107,7 @@ jobs: # Docs: <https://git.io/JvxXE>
needs: [golangci-lint, go-test, validate-config-file]
steps:
- uses: actions/setup-go@v3
with: {go-version: 1.18}
with: {go-version: 1.19}
- uses: actions/checkout@v3
@ -129,7 +133,7 @@ jobs: # Docs: <https://git.io/JvxXE>
- name: Try to execute
if: matrix.os == 'linux'
run: ./error-pages version && ./error-pages -h
run: ./error-pages --version && ./error-pages -h
- uses: actions/upload-artifact@v3
with:
@ -155,7 +159,7 @@ jobs: # Docs: <https://git.io/JvxXE>
run: mv ./error-pages ./../error-pages && chmod +x ./../error-pages
- name: Run generator
run: ./error-pages build ./out --verbose --index
run: ./error-pages --verbose build --index ./out
- name: Test files creation
run: |
@ -210,7 +214,7 @@ jobs: # Docs: <https://git.io/JvxXE>
name: docker-image
path: .artifact
- uses: aquasecurity/trivy-action@0.6.1 # action page: <https://github.com/aquasecurity/trivy-action>
- uses: aquasecurity/trivy-action@0.8.0 # action page: <https://github.com/aquasecurity/trivy-action>
with:
input: .artifact/docker-image.tar
format: sarif
@ -247,7 +251,7 @@ jobs: # Docs: <https://git.io/JvxXE>
- 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: 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
if: always()

View File

@ -43,7 +43,6 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bidichk # Checks for dangerous unicode character sequences
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection
@ -78,7 +77,6 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- unconvert # Remove unnecessary type conversions
- unused # Checks Go code for unused constants, variables, functions and types
- varcheck # Finds unused global variables and constants
- whitespace # Tool for detection of leading and trailing whitespace
- wsl # Whitespace Linter - Forces you to use empty lines!

View File

@ -4,6 +4,34 @@ All notable changes to this package will be documented in this file.
The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver].
## v2.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

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1.2
# Image page: <https://hub.docker.com/_/golang>
FROM golang:1.18-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" .`
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 \
&& go version \
&& CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/ \
&& ./error-pages version \
&& ./error-pages --version \
&& ./error-pages -h
WORKDIR /tmp/rootfs
@ -38,7 +38,7 @@ WORKDIR /tmp/rootfs/opt
# generate static error pages (for usage inside another docker images, for example)
RUN set -x \
&& ./../bin/error-pages --config-file ./error-pages.yml build ./html --verbose --index \
&& ./../bin/error-pages --verbose build --config-file ./error-pages.yml --index ./html \
&& ls -l ./html
# use empty filesystem
@ -72,8 +72,8 @@ ENV LISTEN_PORT="8080" \
DISABLE_L10N="false"
# Docs: <https://docs.docker.com/engine/reference/builder/#healthcheck>
HEALTHCHECK --interval=7s --timeout=2s CMD ["/bin/error-pages", "healthcheck", "--log-json"]
HEALTHCHECK --interval=7s --timeout=2s CMD ["/bin/error-pages", "--log-json", "healthcheck"]
ENTRYPOINT ["/bin/error-pages"]
CMD ["serve", "--log-json"]
CMD ["--log-json", "serve"]

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 ./...
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

View File

@ -5,8 +5,8 @@
<p align="center">
<a href="#"><img src="https://img.shields.io/github/go-mod/go-version/tarampampam/error-pages?longCache=true&label=&logo=go&logoColor=white&style=flat-square" alt="" /></a>
<a href="https://codecov.io/gh/tarampampam/error-pages"><img src="https://img.shields.io/codecov/c/github/tarampampam/error-pages/master.svg?maxAge=30&label=&logo=codecov&logoColor=white&style=flat-square" alt="" /></a>
<a href="https://github.com/tarampampam/error-pages/actions"><img src="https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests?maxAge=30&label=tests&logo=github&style=flat-square" alt="" /></a>
<a href="https://github.com/tarampampam/error-pages/actions"><img src="https://img.shields.io/github/workflow/status/tarampampam/error-pages/release?maxAge=30&label=release&logo=github&style=flat-square" alt="" /></a>
<a href="https://github.com/tarampampam/error-pages/actions"><img src="https://img.shields.io/github/actions/workflow/status/tarampampam/error-pages/tests.yml?branch=master&maxAge=30&label=tests&logo=github&style=flat-square" alt="" /></a>
<a href="https://github.com/tarampampam/error-pages/actions"><img src="https://img.shields.io/github/actions/workflow/status/tarampampam/error-pages/release.yml?maxAge=30&label=release&logo=github&style=flat-square" alt="" /></a>
<a href="https://hub.docker.com/r/tarampampam/error-pages"><img src="https://img.shields.io/docker/pulls/tarampampam/error-pages.svg?maxAge=30&label=pulls&logo=docker&logoColor=white&style=flat-square" alt="" /></a>
<a href="https://hub.docker.com/r/tarampampam/error-pages"><img src="https://img.shields.io/docker/image-size/tarampampam/error-pages/latest?maxAge=30&label=size&logo=docker&logoColor=white&style=flat-square" alt="" /></a>
<a href="https://github.com/tarampampam/error-pages/blob/master/LICENSE"><img src="https://img.shields.io/github/license/tarampampam/error-pages.svg?maxAge=30&style=flat-square" alt="" /></a>
@ -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
- Fully configurable (take a look at the [configuration file](https://github.com/tarampampam/error-pages/blob/master/error-pages.yml) and [project Wiki][wiki])
- Distributed using docker image and compiled binary files
- Localized (🇺🇸, 🇫🇷, 🇺🇦, 🇷🇺, 🇵🇹, 🇳🇱, 🇩🇪, 🇪🇸) HTML error pages (translation process [described here](https://github.com/tarampampam/error-pages/tree/master/l10n) - other translations are welcome!)
- Localized (🇺🇸, 🇫🇷, 🇺🇦, 🇷🇺, 🇵🇹, 🇳🇱, 🇩🇪, 🇪🇸, 🇨🇳) HTML error pages (translation process [described here](https://github.com/tarampampam/error-pages/tree/master/l10n) - other translations are welcome!)
## 🧩 Install
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 |
|-----------------------------------|-----------------------------------|
| [Docker Hub][docker-hub] | `tarampampam/error-pages` |

View File

@ -1,13 +1,22 @@
package main
import (
crypto "crypto/rand"
"encoding/binary"
"math/rand"
"os"
"path/filepath"
"time"
"github.com/fatih/color"
"go.uber.org/automaxprocs/maxprocs"
"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.
var exitFn = os.Exit //nolint:gochecknoglobals
@ -17,9 +26,16 @@ func main() { exitFn(run()) }
// run this CLI application.
// Exit codes documentation: <https://tldp.org/LDP/abs/html/exitcodes.html>
func run() int {
cmd := cli.NewCommand(filepath.Base(os.Args[0]))
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())
return 1

View File

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

View File

@ -8,7 +8,7 @@ volumes:
services:
app: &app-service
image: golang:1.18-buster # Image page: <https://hub.docker.com/_/golang>
image: golang:1.19-buster # Image page: <https://hub.docker.com/_/golang>
working_dir: /src
environment:
HOME: /tmp
@ -31,7 +31,7 @@ services:
start_period: 5s
golint:
image: golangci/golangci-lint:v1.48-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:
GOLANGCI_LINT_CACHE: /tmp/golint # <https://github.com/golangci/golangci-lint/blob/v1.42.0/internal/cache/default.go#L68>
volumes:
@ -41,7 +41,7 @@ services:
command: /bin/true
hurl:
image: orangeopensource/hurl:1.6.1
image: orangeopensource/hurl:1.8.0
volumes:
- .:/src:ro
working_dir: /src

33
go.mod
View File

@ -1,20 +1,21 @@
module github.com/tarampampam/error-pages
go 1.18
go 1.19
require (
github.com/a8m/envsubst v1.3.0
github.com/fasthttp/router v1.4.11
github.com/fasthttp/router v1.4.14
github.com/fatih/color v1.13.0
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.12.2
github.com/prometheus/client_model v0.2.0
github.com/spf13/cobra v1.5.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.0
github.com/valyala/fasthttp v1.38.0
go.uber.org/zap v1.21.0
github.com/prometheus/client_golang v1.14.0
github.com/prometheus/client_model v0.3.0
github.com/stretchr/testify v1.8.1
github.com/urfave/cli/v2 v2.24.1
github.com/valyala/fasthttp v1.43.0
go.uber.org/automaxprocs v1.5.1
go.uber.org/goleak v1.1.11
go.uber.org/zap v1.24.0
gopkg.in/yaml.v3 v3.0.1
)
@ -22,20 +23,22 @@ require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // 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/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
google.golang.org/protobuf v1.27.1 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)

73
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/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -57,6 +56,7 @@ 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/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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp/router v1.4.11 h1:99BvgVxeS2oOZBHnKr/okpdPq1jkn8WvYA2trh/71LY=
github.com/fasthttp/router v1.4.11/go.mod h1:luEEYkGBSAmYyPaMeIUGNgqY+FdHHYDOK9Kivaw7aNo=
github.com/fasthttp/router v1.4.14 h1:+W65VCKgyI4BZszhDiCRfONoFieePZIoQ7D8vGhiuzM=
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/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
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.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.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.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.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
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/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.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.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.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -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.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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.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/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
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.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
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.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=
@ -182,54 +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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 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.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_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-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/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.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/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/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d h1:Q+gqLBOPkFGHyCJxXMRqtUgUbTjI8/Ze8vu8GGyNFwo=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.38.0 h1:yTjSSNjuDi2PPvXY2836bIwLmiTS2T4T9p1coQshpco=
github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
github.com/valyala/fasthttp v1.42.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
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/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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -241,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.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
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/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -275,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-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
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-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@ -316,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-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-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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-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-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -376,9 +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-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -431,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-20200804011535-6c149bb5ef0d/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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@ -512,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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
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/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=
@ -524,12 +538,9 @@ 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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -3,11 +3,12 @@ package checkers_test
import (
"bytes"
"context"
"io/ioutil"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"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"))
return &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
Body: io.NopCloser(bytes.NewReader([]byte{})),
StatusCode: http.StatusOK,
}, nil
}
@ -35,7 +36,7 @@ func TestHealthChecker_CheckSuccess(t *testing.T) {
func TestHealthChecker_CheckFail(t *testing.T) {
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
return &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
Body: io.NopCloser(bytes.NewReader([]byte{})),
StatusCode: http.StatusBadGateway,
}, nil
}

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

View File

@ -1,7 +1,26 @@
package build_test
import "testing"
import (
"flag"
"testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"go.uber.org/goleak"
"go.uber.org/zap"
"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
import (
"fmt"
"strconv"
"errors"
"math"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/tarampampam/error-pages/internal/env"
"github.com/urfave/cli/v2"
"github.com/tarampampam/error-pages/internal/cli/shared"
)
type checker interface {
Check(port uint16) error
}
const portFlagName = "port"
// NewCommand creates `healthcheck` command.
func NewCommand(checker checker) *cobra.Command {
var port uint16
cmd := &cobra.Command{
Use: "healthcheck",
func NewCommand(checker checker) *cli.Command {
return &cli.Command{
Name: "healthcheck",
Aliases: []string{"chk", "health", "check"},
Short: "Health checker for the HTTP server. Use case - docker healthcheck",
PreRunE: func(c *cobra.Command, _ []string) (lastErr error) {
c.Flags().VisitAll(func(flag *pflag.Flag) {
// flag was NOT defined using CLI (flags should have maximal priority)
if !flag.Changed && flag.Name == portFlagName {
if envPort, exists := env.ListenPort.Lookup(); exists && envPort != "" {
if p, err := strconv.ParseUint(envPort, 10, 16); err == nil { //nolint:gomnd
port = uint16(p)
} else {
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envPort)
}
}
}
})
Usage: "Health checker for the HTTP server. Use case - docker healthcheck",
Action: func(c *cli.Context) error {
var port = c.Uint(shared.ListenPortFlag.Name)
return lastErr
if port <= 0 || port > math.MaxUint16 {
return errors.New("port value out of range")
}
return checker.Check(uint16(port))
},
RunE: func(*cobra.Command, []string) error {
return checker.Check(port)
Flags: []cli.Flag{
shared.ListenPortFlag,
},
}
cmd.Flags().Uint16VarP(
&port,
portFlagName,
"p",
8080, //nolint:gomnd // must be same as default serve `--port` flag value
fmt.Sprintf("TCP port number [$%s]", env.ListenPort),
)
return cmd
}

View File

@ -2,12 +2,12 @@ package healthcheck_test
import (
"errors"
"os"
"flag"
"testing"
"github.com/kami-zh/go-capturer"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"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) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
assert.Equal(t, "healthcheck", cmd.Use)
assert.Equal(t, "healthcheck", cmd.Name)
assert.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases)
assert.NotNil(t, cmd.RunE)
assert.NotNil(t, cmd.Action)
}
func TestCommandRun(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
cmd.SetArgs([]string{})
output := capturer.CaptureOutput(func() {
assert.NoError(t, cmd.Execute())
})
assert.Empty(t, output)
assert.NoError(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)))
}
func TestCommandRunFailed(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: errors.New("foo err")})
cmd.SetArgs([]string{})
output := capturer.CaptureStderr(func() {
assert.Error(t, cmd.Execute())
})
assert.Contains(t, output, "foo err")
assert.ErrorContains(t, cmd.Run(cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)), "foo err")
}
func TestPortFlagWrongArgument(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
cmd.SetArgs([]string{"-p", "65536"}) // 65535 is max
var executed bool
err := cmd.Run(
cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil),
"", "-p", "65536",
)
cmd.RunE = func(*cobra.Command, []string) error {
executed = true
return nil
}
output := capturer.CaptureStderr(func() {
assert.Error(t, cmd.Execute())
})
assert.Contains(t, output, "invalid argument")
assert.Contains(t, output, "65536")
assert.Contains(t, output, "value out of range")
assert.False(t, executed)
}
func TestPortFlagWrongEnvValue(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
cmd.SetArgs([]string{})
assert.NoError(t, os.Setenv("LISTEN_PORT", "65536")) // 65535 is max
defer func() { assert.NoError(t, os.Unsetenv("LISTEN_PORT")) }()
var executed bool
cmd.RunE = func(*cobra.Command, []string) error {
executed = true
return nil
}
output := capturer.CaptureStderr(func() {
assert.Error(t, cmd.Execute())
})
assert.Contains(t, output, "wrong TCP port")
assert.Contains(t, output, "environment variable")
assert.Contains(t, output, "65536")
assert.False(t, executed)
assert.ErrorContains(t, err, "port value out of range")
}

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 (
"context"
"errors"
"fmt"
"net"
"os"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/tarampampam/error-pages/internal/breaker"
"github.com/tarampampam/error-pages/internal/config"
appHttp "github.com/tarampampam/error-pages/internal/http"
"github.com/tarampampam/error-pages/internal/pick"
"github.com/urfave/cli/v2"
"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.
func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command {
var (
f flags
cfg *config.Config
)
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
var cmd = command{}
cmd := &cobra.Command{
Use: "serve",
cmd.c = &cli.Command{
Name: "serve",
Aliases: []string{"s", "server"},
Short: "Start HTTP server",
PreRunE: func(cmd *cobra.Command, _ []string) (err error) {
if configFile == nil {
Usage: "Start HTTP server",
Action: func(c *cli.Context) error {
var cfg *config.Config
if configPath := c.String(shared.ConfigFileFlag.Name); configPath == "" { // load config from file
return errors.New("path to the config file is required for this command")
}
if err = f.OverrideUsingEnv(cmd.Flags()); err != nil {
} else if loadedCfg, err := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name)); err != nil {
return err
} else {
cfg = loadedCfg
}
if cfg, err = config.FromYamlFile(*configFile); err != nil {
return err
var (
ip = c.String(shared.ListenAddrFlag.Name)
port = uint16(c.Uint(shared.ListenPortFlag.Name))
o options.ErrorPage
)
if net.ParseIP(ip) == nil {
return fmt.Errorf("wrong IP address [%s] for listening", ip)
}
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
return cmd.c
}
// run current command.
func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags) error { //nolint:funlen
// Run current command.
func (cmd *command) Run( //nolint:funlen
parentCtx context.Context, log *zap.Logger, cfg *config.Config, ip string, port uint16, opt options.ErrorPage,
) error {
var (
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
oss = breaker.NewOSSignals(ctx) // OS signals listener
@ -70,8 +181,6 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
var (
templateNames = cfg.TemplateNames()
picker interface{ Pick() string }
opt = f.ToOptions()
)
switch opt.Template.Name {
@ -124,8 +233,8 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
defer close(errCh)
log.Info("Server starting",
zap.String("addr", f.Listen.IP),
zap.Uint16("port", f.Listen.Port),
zap.String("addr", ip),
zap.Uint16("port", port),
zap.String("default error page", opt.Default.PageCode),
zap.Uint16("default HTTP response code", opt.Default.HTTPCode),
zap.Strings("proxy headers", opt.ProxyHTTPHeaders),
@ -133,7 +242,7 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
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
}
}(startingErrCh)

View File

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

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
import (
"io/ioutil"
"os"
"path"
"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) 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)
}
@ -236,7 +235,7 @@ func FromYaml(in []byte) (_ *Config, err error) {
// FromYamlFile creates new Config instance using YAML file.
func FromYamlFile(filepath string) (*Config, error) {
bytes, err := ioutil.ReadFile(filepath)
bytes, err := os.ReadFile(filepath)
if err != nil {
return nil, errors.Wrap(err, "cannot read configuration file")
}

3
internal/env/env.go vendored
View File

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

View File

@ -39,7 +39,7 @@ func ClientWantFormat(ctx *fasthttp.RequestCtx) ContentType {
f := format{b[0:idx], 0}
if len(b) > idx+3 {
if weight, err := strconv.ParseFloat(string(b[idx+3:]), 32); err == nil { //nolint:gomnd
if weight, err := strconv.ParseFloat(string(b[idx+3:]), 32); err == nil {
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
import (
"errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// New creates new "zap" logger with little customization.
func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
// New creates new "zap" logger with a small customization.
func New(l Level, f Format) (*zap.Logger, error) {
var config zap.Config
if logJSON {
config = zap.NewProductionConfig()
} else {
switch f {
case ConsoleFormat:
config = zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05")
case JSONFormat:
config = zap.NewProductionConfig() // json encoder is used by default
default:
return nil, errors.New("unsupported logging format")
}
// default configuration for all encoders
@ -24,15 +31,31 @@ func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
config.DisableStacktrace = true
config.DisableCaller = true
if debug {
// enable additional features for debugging
if l <= DebugLevel {
config.Development = true
config.DisableStacktrace = false
config.DisableCaller = false
}
if verbose || debug {
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
var zapLvl zapcore.Level
switch l { // convert level to zap.Level
case DebugLevel:
zapLvl = zap.DebugLevel
case InfoLevel:
zapLvl = zap.InfoLevel
case WarnLevel:
zapLvl = zap.WarnLevel
case ErrorLevel:
zapLvl = zap.ErrorLevel
case FatalLevel:
zapLvl = zap.FatalLevel
default:
return nil, errors.New("unsupported logging level")
}
config.Level = zap.NewAtomicLevelAt(zapLvl)
return config.Build()
}

View File

@ -8,64 +8,51 @@ import (
"github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tarampampam/error-pages/internal/logger"
)
func TestNewNotVerboseDebugJSON(t *testing.T) {
func TestNewDebugLevelConsoleFormat(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(false, false, false)
assert.NoError(t, err)
log, err := logger.New(logger.DebugLevel, logger.ConsoleFormat)
require.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg")
log.Info("inf msg")
log.Error("err msg")
})
assert.Contains(t, output, time.Now().Format("15:04:05"))
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
assert.NotContains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewVerboseNotDebugJSON(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(true, false, false)
assert.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg")
log.Error("err msg")
})
assert.Contains(t, output, time.Now().Format("15:04:05"))
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
assert.Contains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewVerboseDebugNotJSON(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(true, true, false)
assert.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg")
log.Error("err msg")
})
assert.Contains(t, output, time.Now().Format("15:04:05"))
assert.Regexp(t, `\t.+info.+\t.+logger_test\.go:\d+\tinf msg`, output)
assert.Contains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewNotVerboseDebugButJSON(t *testing.T) {
func TestNewErrorLevelConsoleFormat(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(false, false, true)
assert.NoError(t, err)
log, err := logger.New(logger.ErrorLevel, logger.ConsoleFormat)
require.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg")
log.Info("inf msg")
log.Error("err msg")
})
assert.NotContains(t, output, "inf msg")
assert.NotContains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewWarnLevelJSONFormat(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(logger.WarnLevel, logger.JSONFormat)
require.NoError(t, err)
log.Debug("dbg msg")
log.Info("inf msg")
log.Warn("warn msg")
log.Error("err msg")
})
@ -75,6 +62,14 @@ func TestNewNotVerboseDebugButJSON(t *testing.T) {
lines := strings.Split(strings.Trim(output, "\n"), "\n")
assert.JSONEq(t, `{"level":"info","ts":0.1,"msg":"inf msg"}`, lines[0])
assert.JSONEq(t, `{"level":"warn","ts":0.1,"msg":"warn msg"}`, lines[0])
assert.JSONEq(t, `{"level":"error","ts":0.1,"msg":"err msg"}`, lines[1])
}
func TestNewErrors(t *testing.T) {
_, err := logger.New(logger.Level(127), logger.ConsoleFormat)
require.EqualError(t, err, "unsupported logging level")
_, err = logger.New(logger.WarnLevel, logger.Format(255))
require.EqualError(t, err, "unsupported logging format")
}

View File

@ -10,6 +10,7 @@ import (
"time"
"github.com/pkg/errors"
"github.com/tarampampam/error-pages/internal/version"
)
@ -30,6 +31,7 @@ var tplFnMap = template.FuncMap{ //nolint:gochecknoglobals
return 0
},
"env": os.Getenv,
}
var ErrClosed = errors.New("closed")

View File

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

View File

@ -10,6 +10,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Fout',
de: 'Fehler',
es: 'Error',
zh: '错误',
},
'Good luck': {
fr: 'Bonne chance',
@ -19,6 +20,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Veel succes',
de: 'Viel Glück',
es: 'Buena Suerte',
zh: '祝好运',
},
'UH OH': {
fr: 'Oups',
@ -28,6 +30,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Oeps',
de: 'Hoppla',
es: 'Oups',
zh: '哎呀',
},
'Request details': {
fr: 'Détails de la requête',
@ -37,6 +40,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Details van verzoek',
de: 'Details der Anfrage',
es: 'Detalles de la petición',
zh: '请求详情',
},
'Double-check the URL': {
fr: 'Vérifiez lURL',
@ -46,6 +50,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Controleer de URL',
de: 'Überprüfen Sie die URL',
es: 'Verifique la url',
zh: '请再次检查地址',
},
'Alternatively, go back': {
fr: 'Essayer de revenir en arrière',
@ -55,6 +60,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Of ga terug',
de: 'Alternativ gehen Sie zurück',
es: 'Como alternativa, vuelva atrás',
zh: '或返回上一页',
},
'Here\'s what might have happened': {
fr: 'Voici ce qui aurait pu se passer',
@ -64,6 +70,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Vous avez peut-être mal tapé lURL',
@ -73,6 +80,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Le site a été déplacé',
@ -82,6 +90,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'De site is verplaatst',
de: 'Die Seite wurde verschoben',
es: 'El sitio se ha movido',
zh: '站点已被转移',
},
'It was never here': {
fr: 'Il na jamais été ici',
@ -91,6 +100,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Het was hier nooit',
de: 'Es war nie hier',
es: 'Nunca ha estado aquí',
zh: '站点从未存在',
},
'Bad Request': {
fr: 'Mauvaise requête',
@ -99,7 +109,8 @@ Object.defineProperty(window, 'l10n', {
pt: 'Requisição inválida',
nl: 'Foutieve anvraag',
de: 'Fehlerhafte Anfrage',
es: 'Petición inválida'
es: 'Petición inválida',
zh: '错误请求',
},
'The server did not understand the request': {
fr: 'Le serveur ne comprend pas la requête',
@ -108,7 +119,8 @@ Object.defineProperty(window, 'l10n', {
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'
es: 'El servidor no entendió la petición',
zh: '服务器不理解该请求',
},
'Unauthorized': {
fr: 'Non autorisé',
@ -118,6 +130,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Niet geautoriseerd',
de: 'Nicht autorisiert',
es: 'No autorizado',
zh: '未经授权',
},
'The requested page needs a username and a password': {
fr: 'La page demandée nécessite un nom dutilisateur et un mot de passe',
@ -127,6 +140,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Interdit',
@ -136,6 +150,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Verboden',
de: 'Verboten',
es: 'Prohibido',
zh: '禁止访问',
},
'Access is forbidden to the requested page': {
fr: 'Accès interdit à la page demandée',
@ -145,6 +160,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Introuvable',
@ -154,6 +170,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Niet gevonden',
de: 'Nicht gefunden',
es: 'No encontrado',
zh: '未找到',
},
'The server can not find the requested page': {
fr: 'Le serveur ne peut trouver la page demandée',
@ -163,6 +180,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Méthode Non Autorisée',
@ -172,6 +190,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Methode niet toegestaan',
de: 'Methode nicht erlaubt',
es: 'Método no permitido',
zh: '方法不被允许',
},
'The method specified in the request is not allowed': {
fr: 'La méthode spécifiée dans la requête nest pas autorisée',
@ -180,7 +199,8 @@ Object.defineProperty(window, 'l10n', {
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'
es: 'El método especificado en la petición no está permitido',
zh: '请求指定的方法不被允许',
},
'Proxy Authentication Required': {
fr: 'Authentification proxy requise',
@ -190,6 +210,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Vous devez vous authentifier avec un serveur proxy avant que cette requête puisse être servie',
@ -199,6 +220,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Requête expiré',
@ -208,6 +230,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'La requête prend plus de temps que prévu',
@ -217,6 +240,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Conflit',
@ -226,6 +250,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Conflict',
de: 'Konflikt',
es: 'Conflicto',
zh: '冲突',
},
'The request could not be completed because of a conflict': {
fr: 'La requête na pas pu être complétée à cause dun conflit',
@ -235,6 +260,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Supprimé',
@ -244,6 +270,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Verdwenen',
de: 'Verschwunden',
es: 'Eliminado',
zh: '已移除',
},
'The requested page is no longer available': {
fr: 'La page demandée nest plus disponible',
@ -253,6 +280,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Longueur requise',
@ -262,6 +290,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Le "Content-Length" nest pas défini. Le serveur ne prendra pas en compte la requête',
@ -271,6 +300,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Échec de la condition préalable',
@ -280,6 +310,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Niet voldaan aan vooraf gestelde voorwaarde',
de: 'Vorbedingung fehlgeschlagen',
es: 'Precondición fallida',
zh: '前置条件判定失败',
},
'The pre condition given in the request evaluated to false by the server': {
fr: 'La précondition donnée dans la requête a été évaluée comme étant fausse par le serveur',
@ -288,7 +319,8 @@ Object.defineProperty(window, 'l10n', {
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'
es: 'La precondición ha sido evaluada como negativa para esta petición por el servidor',
zh: '服务器评估请求中给出的前置条件的结果为false(假)',
},
'Payload Too Large': {
fr: 'Charge trop volumineuse',
@ -298,6 +330,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Le serveur ne prendra pas en compte la requête, car lentité de la requête est trop volumineuse',
@ -307,6 +340,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Requête non satisfaisante',
@ -316,6 +350,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Le byte range demandé nest pas disponible et est hors des limites',
@ -325,6 +360,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Je suis une théière',
@ -333,7 +369,8 @@ Object.defineProperty(window, 'l10n', {
pt: 'Eu sou um bule',
nl: 'Ik ben een theepot',
de: 'Ich bin eine Teekanne',
es: 'Soy una tetera'
es: 'Soy una tetera',
zh: '我是一只茶壶',
},
'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',
@ -343,6 +380,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Trop de requêtes',
@ -351,7 +389,8 @@ Object.defineProperty(window, 'l10n', {
pt: 'Excesso de solicitações',
nl: 'Te veel requests',
de: 'Zu viele Anfragen',
es: 'Demasiadas peticiones'
es: 'Demasiadas peticiones',
zh: '请求过多',
},
'Too many requests in a given amount of time': {
fr: 'Trop de requêtes dans un délai donné',
@ -361,6 +400,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Erreur interne du serveur',
@ -370,6 +410,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Interne serverfout',
de: 'Interner Server-Fehler',
es: 'Error Interno',
zh: '内部服务器错误',
},
'The server met an unexpected condition': {
fr: 'Le serveur a rencontré une condition inattendue',
@ -379,6 +420,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Mauvaise passerelle',
@ -388,6 +430,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Ongeldige Gateway',
de: 'Fehlerhafter Gateway',
es: 'Puerta de enlace no valida',
zh: '无效网关',
},
'The server received an invalid response from the upstream server': {
fr: 'Le serveur a reçu une réponse invalide du serveur distant',
@ -396,7 +439,8 @@ Object.defineProperty(window, 'l10n', {
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'
es: 'El servidor ha recibido una respuesta no válida del servidor de origen',
zh: '服务器从上游服务器收到了无效的响应',
},
'Service Unavailable': {
fr: 'Service indisponible',
@ -406,6 +450,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Dienst niet beschikbaar',
de: 'Dienst nicht verfügbar',
es: 'Servicio no disponible',
zh: '服务不可用',
},
'The server is temporarily overloading or down': {
fr: 'Le serveur est temporairement en surcharge ou indisponible',
@ -415,6 +460,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Expiration Passerelle',
@ -424,6 +470,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Gateway Verlopen',
de: 'Gateway Zeitüberschreitung',
es: 'Tiempo límite de puerta de enlace excedido',
zh: '网关超时',
},
'The gateway has timed out': {
fr: 'Le temps dattente de la passerelle est dépassé',
@ -433,6 +480,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Version HTTP non prise en charge',
@ -442,6 +490,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Le serveur ne supporte pas la version du protocole HTTP',
@ -451,6 +500,7 @@ Object.defineProperty(window, 'l10n', {
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': {
@ -461,6 +511,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Host',
de: 'Host',
es: 'Host',
zh: '主机',
},
'Original URI': {
fr: 'URI dorigine',
@ -470,6 +521,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Originele URI',
de: 'Originale URI',
es: 'URI original',
zh: '原始URI',
},
'Forwarded for': {
fr: 'Transmis pour',
@ -479,6 +531,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Doorgestuurd voor',
de: 'Weitergeleitet für',
es: 'Remitido por',
zh: '转发自',
},
'Namespace': {
fr: 'Espace de noms',
@ -488,6 +541,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Elementnaam',
de: 'Namensraum',
es: 'Namespace',
zh: '命名空间',
},
'Ingress name': {
fr: 'Nom ingress',
@ -497,6 +551,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Ingress naam',
de: 'Ingress Name',
es: 'Nombre Ingress',
zh: '入口名',
},
'Service name': {
fr: 'Nom du service',
@ -505,7 +560,8 @@ Object.defineProperty(window, 'l10n', {
pt: 'Nome do Serviço',
nl: 'Service naam',
de: 'Service Name',
es: 'Nombre del servicio'
es: 'Nombre del servicio',
zh: '服务名',
},
'Service port': {
fr: 'Port du service',
@ -515,6 +571,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Service poort',
de: 'Service Port',
es: 'Puerto del servicio',
zh: '服务端口',
},
'Request ID': {
fr: 'Identifiant de la requête',
@ -524,6 +581,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'ID van het verzoek',
de: 'Anfrage ID',
es: 'ID de la petición',
zh: '请求ID',
},
'Timestamp': {
fr: 'Horodatage',
@ -533,6 +591,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Tijdstempel',
de: 'Zeitstempel',
es: 'Timestamp',
zh: '时间戳',
},
'client-side error': {
@ -543,6 +602,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'fout aan de gebruikerskant',
de: 'Clientseitiger Fehler',
es: 'Error del lado del cliente',
zh: '客户端错误',
},
'server-side error': {
fr: 'Erreur Serveur',
@ -552,6 +612,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'fout aan de serverkant',
de: 'Serverseitiger Fehler',
es: 'Error del lado del servidor',
zh: '服务端错误',
},
'Your Client': {
@ -562,6 +623,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Jouw Client',
de: 'Ihr Client',
es: 'Tu cliente',
zh: '您的客户端',
},
'Network': {
fr: 'Réseau',
@ -571,6 +633,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Netwerk',
de: 'Netzwerk',
es: 'Red',
zh: '网络',
},
'Web Server': {
fr: 'Serveur Web',
@ -580,6 +643,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Web Server',
de: 'Webserver',
es: 'Servidor web',
zh: 'Web服务器',
},
'What happened?': {
fr: 'Que sest-il passé ?',
@ -589,6 +653,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Wat is er gebeurd?',
de: 'Was ist passiert?',
es: '¿Que ha pasado?',
zh: '发生了什么?',
},
'What can i do?': {
fr: 'Que puis-je faire ?',
@ -598,6 +663,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Wat kan ik doen?',
de: 'Was kann ich machen?',
es: '¿Que puedo hacer?',
zh: '我能做什么?',
},
'Please try again in a few minutes': {
fr: 'Veuillez réessayer dans quelques minutes',
@ -607,6 +673,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Opérationnel',
@ -616,6 +683,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Functioneel',
de: 'Funktioniert',
es: 'Trabajando',
zh: '正常运行',
},
'Unknown': {
fr: 'Inconnu',
@ -625,6 +693,7 @@ Object.defineProperty(window, 'l10n', {
nl: 'Onbekend',
de: 'Unbekannt',
es: 'Desconocido',
zh: '未知',
},
'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',
@ -633,7 +702,8 @@ Object.defineProperty(window, 'l10n', {
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'
es: 'Por favor intente cambiar el método de la petición, cabeceras, carga o URL',
zh: '请尝试更改请求方法、标头、有效负载或URL',
},
'Please check your authorization data': {
fr: 'Veuillez vérifier vos données dautorisation',
@ -643,6 +713,7 @@ Object.defineProperty(window, 'l10n', {
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': {
fr: 'Veuillez vérifier lURL et réessayer',
@ -652,6 +723,7 @@ Object.defineProperty(window, 'l10n', {
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

@ -18,3 +18,4 @@ By default, pages markup contains strings in English (`en` locale). If you want
- 🇳🇱 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)