Compare commits

...

258 Commits

Author SHA1 Message Date
05a513898a fix: 🐛 Ignore URL parameters 2024-07-01 23:59:10 +04:00
3c5b8f6337 test: 🧪 Missing tests added (existing corrected) 2024-07-01 23:44:27 +04:00
df2163e41e feat: fasthttp is back 2024-07-01 23:41:26 +04:00
afaef54ddf wip: 🔕 temporary commit 2024-06-30 18:12:52 +04:00
e3499a940c wip: 🔕 temporary commit 2024-06-29 21:47:20 +04:00
f3c2646f4e wip: 🔕 temporary commit 2024-06-29 21:33:43 +04:00
e91a6f99fd wip: 🔕 temporary commit 2024-06-29 21:12:08 +04:00
2664c467c0 wip: 🔕 temporary commit 2024-06-29 21:09:56 +04:00
52063654ab wip: 🔕 temporary commit 2024-06-29 20:47:18 +04:00
616e4ad7e2 wip: 🔕 temporary commit 2024-06-29 20:44:08 +04:00
1563a8c226 wip: 🔕 temporary commit 2024-06-29 20:32:37 +04:00
8b29a95bb1 wip: 🔕 temporary commit 2024-06-29 20:25:58 +04:00
b7ed431c8f wip: 🔕 temporary commit 2024-06-29 20:06:06 +04:00
6766f3f787 wip: 🔕 temporary commit 2024-06-29 19:55:55 +04:00
75476a22c0 wip: 🔕 temporary commit 2024-06-29 19:53:40 +04:00
4e653f37d3 wip: 🔕 temporary commit 2024-06-29 19:50:23 +04:00
4e7abaafbf wip: 🔕 temporary commit 2024-06-29 19:45:35 +04:00
9e719c7feb wip: 🔕 temporary commit 2024-06-29 19:38:13 +04:00
cbbe2ea631 wip: 🔕 temporary commit 2024-06-29 19:37:24 +04:00
4bb567b1d6 wip: 🔕 temporary commit 2024-06-29 19:11:21 +04:00
cea34f0475 wip: 🔕 temporary commit 2024-06-29 16:34:03 +04:00
86aa4aab93 wip: 🔕 temporary commit 2024-06-29 15:07:53 +04:00
7ed6ec414f wip: 🔕 temporary commit 2024-06-29 15:04:38 +04:00
14815b4c73 wip: 🔕 temporary commit 2024-06-29 14:54:47 +04:00
b7228c3933 wip: 🔕 temporary commit 2024-06-29 02:59:47 +04:00
1fa6e1de4f wip: 🔕 temporary commit 2024-06-29 00:49:59 +04:00
5976f2903d wip: 🔕 temporary commit 2024-06-28 19:28:35 +04:00
63b125e080 wip: 🔕 temporary commit 2024-06-28 16:57:06 +04:00
a759504971 wip: 🔕 temporary commit 2024-06-28 16:55:06 +04:00
2776c41e0d wip: 🔕 temporary commit 2024-06-28 16:25:43 +04:00
42c4e7bf84 wip: 🔕 temporary commit 2024-06-27 19:29:54 +04:00
c80229ea05 wip: 🔕 temporary commit 2024-06-27 00:13:17 +04:00
f1bcd0d8c4 wip: 🔕 temporary commit 2024-06-26 23:08:07 +04:00
3505288e7a wip: 🔕 temporary commit 2024-06-26 22:35:48 +04:00
ea7dfe4870 wip: 🔕 temporary commit 2024-06-26 21:45:12 +04:00
6326e78cf4 wip: 🔕 temporary commit 2024-06-26 20:03:44 +04:00
3f22916cbb wip: 🔕 temporary commit 2024-06-26 17:59:19 +04:00
015e686635 wip: 🔕 temporary commit 2024-06-26 14:52:21 +04:00
2a1fa5c108 wip: 🔕 temporary commit 2024-06-26 01:06:02 +04:00
1682a3513f wip: 🔕 temporary commit 2024-06-25 22:26:34 +04:00
65fc5ecc7f wip: 🔕 temporary commit 2024-06-25 18:29:18 +04:00
c1eaee0287 wip: 🔕 temporary commit 2024-06-25 00:09:22 +04:00
ceeb7f9384 feat: Use slog instead of zap 2024-06-24 19:28:03 +04:00
a52dbde00c wip: 🔕 temporary commit 2024-06-23 23:48:51 +04:00
1b94bc367c wip: 🔕 temporary commit 2024-06-23 16:12:06 +04:00
669aaf6a1e wip: 🔕 temporary commit 2024-06-23 00:05:11 +04:00
b71475fcf7 wip: 🔕 temporary commit 2024-06-22 12:05:01 +04:00
71f8cfc162 wip: 🔕 temporary commit 2024-06-22 03:32:10 +04:00
e2193cd82e wip: 🔕 temporary commit 2024-06-21 17:13:27 +04:00
15d1bcf9c7 chore(docker): The docker env re-written 2024-06-20 23:09:32 +04:00
e6f49f622d fix(ci): Disable cache for setup-node github action 2024-06-20 14:16:57 +04:00
985fc18a48 chore(ci): Switch from gacts/setup-node-with-cache to actions/setup-node action 2024-06-20 14:07:49 +04:00
5512f2d8bb build(deps): bump the github-actions group with 2 updates (#283) 2024-06-02 09:07:46 +00:00
d585b531cb Merge pull request #282 from tarampampam/dependabot/go_modules/gomod-86828b0f1c
build(deps): bump the gomod group with 4 updates
2024-06-01 22:26:00 +00:00
31b64ff3e9 build(deps): bump the gomod group with 4 updates
Bumps the gomod group with 4 updates: [github.com/fasthttp/router](https://github.com/fasthttp/router), [github.com/fatih/color](https://github.com/fatih/color), [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) and [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp).


Updates `github.com/fasthttp/router` from 1.5.0 to 1.5.1
- [Release notes](https://github.com/fasthttp/router/releases)
- [Commits](https://github.com/fasthttp/router/compare/v1.5.0...v1.5.1)

Updates `github.com/fatih/color` from 1.16.0 to 1.17.0
- [Release notes](https://github.com/fatih/color/releases)
- [Commits](https://github.com/fatih/color/compare/v1.16.0...v1.17.0)

Updates `github.com/prometheus/client_golang` from 1.19.0 to 1.19.1
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.19.0...v1.19.1)

Updates `github.com/valyala/fasthttp` from 1.52.0 to 1.54.0
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.52.0...1.54.0)

---
updated-dependencies:
- dependency-name: github.com/fasthttp/router
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
- dependency-name: github.com/fatih/color
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-01 22:25:44 +00:00
98d1a5bf6e chore(deps): update golangci/golangci-lint docker tag to v1.59 (#281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-27 23:51:34 +04:00
09db299d37 chore(deps): update golangci/golangci-lint docker tag to v1.58 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-05 10:09:54 +04:00
676c65e66a Merge pull request #279 from tarampampam/dependabot/go_modules/gomod-34457f1dff
build(deps): bump github.com/urfave/cli/v2 from 2.27.1 to 2.27.2 in the gomod group
2024-05-01 22:26:24 +00:00
f6fe108380 build(deps): bump github.com/urfave/cli/v2 in the gomod group
Bumps the gomod group with 1 update: [github.com/urfave/cli/v2](https://github.com/urfave/cli).


Updates `github.com/urfave/cli/v2` from 2.27.1 to 2.27.2
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.27.1...v2.27.2)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 22:26:10 +00:00
506b4d6ab5 build(deps): bump the github-actions group with 2 updates (#278) 2024-04-27 10:54:51 +00:00
9f552a7bec chore(deps): update ghcr.io/orange-opensource/hurl docker tag to v4.3.0 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-27 14:52:16 +04:00
b2eb35627b Merge pull request #277 from tarampampam/dependabot/go_modules/gomod-a0bc6120b0
build(deps): bump github.com/prometheus/client_model from 0.6.0 to 0.6.1 in the gomod group
2024-04-27 10:35:22 +00:00
4309bede7c build(deps): bump github.com/prometheus/client_model in the gomod group
Bumps the gomod group with 1 update: [github.com/prometheus/client_model](https://github.com/prometheus/client_model).


Updates `github.com/prometheus/client_model` from 0.6.0 to 0.6.1
- [Release notes](https://github.com/prometheus/client_model/releases)
- [Commits](https://github.com/prometheus/client_model/compare/v0.6.0...v0.6.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_model
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-27 10:35:10 +00:00
cf929abebd Update dependabot.yml 2024-04-27 14:34:46 +04:00
70847336ff build(deps): bump the any group with 2 updates (#273) 2024-04-02 05:49:17 +00:00
add7da3fe1 chore(deps): update golangci/golangci-lint docker tag to v1.57 (#272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-19 21:48:27 -07:00
abd647e975 Update dependabot.yml 2024-03-19 09:52:17 +04:00
c851aad4f2 hotfix for the release error 2024-03-17 11:39:51 +04:00
0c7e766f93 build(deps): bump golangci/golangci-lint-action from 3 to 4 (#268) 2024-03-16 16:46:45 +00:00
4d9db28c78 Merge pull request #270 from tarampampam/dependabot/go_modules/github.com/fasthttp/router-1.5.0
build(deps): bump github.com/fasthttp/router from 1.4.22 to 1.5.0
2024-03-16 15:11:55 +00:00
1c6c35c1db build(deps): bump github.com/fasthttp/router from 1.4.22 to 1.5.0
Bumps [github.com/fasthttp/router](https://github.com/fasthttp/router) from 1.4.22 to 1.5.0.
- [Release notes](https://github.com/fasthttp/router/releases)
- [Commits](https://github.com/fasthttp/router/compare/v1.4.22...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/fasthttp/router
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-16 15:11:42 +00:00
ed8303a1a6 Merge pull request #269 from tarampampam/dependabot/github_actions/aquasecurity/trivy-action-0.18.0
build(deps): bump aquasecurity/trivy-action from 0.16.1 to 0.18.0
2024-03-01 22:38:56 +00:00
db7969d4bc build(deps): bump aquasecurity/trivy-action from 0.16.1 to 0.18.0
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.16.1 to 0.18.0.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.16.1...0.18.0)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 22:38:41 +00:00
0b0b0c2bca Merge pull request #267 from tarampampam/dependabot/go_modules/github.com/valyala/fasthttp-1.52.0
build(deps): bump github.com/valyala/fasthttp from 1.51.0 to 1.52.0
2024-03-01 22:08:47 +00:00
5b23d767c7 build(deps): bump github.com/valyala/fasthttp from 1.51.0 to 1.52.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.51.0 to 1.52.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.51.0...v1.52.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 22:08:32 +00:00
8065125526 Merge pull request #265 from tarampampam/dependabot/go_modules/github.com/stretchr/testify-1.9.0
build(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0
2024-03-01 22:08:16 +00:00
ba5faed23c build(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 22:08:05 +00:00
5c9ffba5de Merge pull request #266 from tarampampam/dependabot/go_modules/github.com/prometheus/client_model-0.6.0
build(deps): bump github.com/prometheus/client_model from 0.5.0 to 0.6.0
2024-03-01 22:07:46 +00:00
5697f2c1a2 build(deps): bump github.com/prometheus/client_model from 0.5.0 to 0.6.0
Bumps [github.com/prometheus/client_model](https://github.com/prometheus/client_model) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/prometheus/client_model/releases)
- [Commits](https://github.com/prometheus/client_model/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_model
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 22:07:33 +00:00
b477db7af0 Merge pull request #264 from tarampampam/dependabot/go_modules/github.com/prometheus/client_golang-1.19.0
build(deps): bump github.com/prometheus/client_golang from 1.18.0 to 1.19.0
2024-03-01 22:06:48 +00:00
cc0b862c5a Merge pull request #263 from tarampampam/dependabot/go_modules/go.uber.org/zap-1.27.0
build(deps): bump go.uber.org/zap from 1.26.0 to 1.27.0
2024-03-01 22:06:42 +00:00
46a8b0002e build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.19.0/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 22:06:36 +00:00
544ae0c7ab build(deps): bump go.uber.org/zap from 1.26.0 to 1.27.0
Bumps [go.uber.org/zap](https://github.com/uber-go/zap) from 1.26.0 to 1.27.0.
- [Release notes](https://github.com/uber-go/zap/releases)
- [Changelog](https://github.com/uber-go/zap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/zap/compare/v1.26.0...v1.27.0)

---
updated-dependencies:
- dependency-name: go.uber.org/zap
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 22:06:30 +00:00
8dfc68dd6f Merge pull request #262 from tarampampam/dependabot/docker/golang-1.22-bullseye
build(deps): bump golang from 1.21-bullseye to 1.22-bullseye
2024-03-01 22:02:06 +00:00
7beea7b5a8 build(deps): bump golang from 1.21-bullseye to 1.22-bullseye
Bumps golang from 1.21-bullseye to 1.22-bullseye.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 22:01:50 +00:00
b848ea7525 chore(deps): update golangci/golangci-lint docker tag to v1.56 (#261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-08 09:57:10 +04:00
e9b7884daf build(deps): bump codecov/codecov-action from 3 to 4 (#260) 2024-02-01 22:48:40 +00:00
b8b58fc129 build(deps): bump peter-evans/dockerhub-description from 3 to 4 (#258) 2024-02-01 22:48:26 +00:00
f49da87a1f Merge pull request #259 from tarampampam/dependabot/github_actions/aquasecurity/trivy-action-0.16.1
build(deps): bump aquasecurity/trivy-action from 0.16.0 to 0.16.1
2024-02-01 22:17:35 +00:00
bff018f66a build(deps): bump aquasecurity/trivy-action from 0.16.0 to 0.16.1
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.16.0...0.16.1)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-01 22:17:21 +00:00
b8f9608992 build(deps): bump actions/upload-artifact from 3 to 4 (#252) 2024-01-02 08:15:22 +00:00
f22855fee6 build(deps): bump github/codeql-action from 2 to 3 (#251) 2024-01-02 08:12:55 +00:00
b2231cb97c build(deps): bump actions/download-artifact from 3 to 4 (#253)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 12:07:20 +04:00
759f70d1f0 Merge pull request #255 from tarampampam/dependabot/go_modules/github.com/prometheus/client_golang-1.18.0
build(deps): bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0
2024-01-01 22:35:50 +00:00
5b74eaa3de build(deps): bump github.com/prometheus/client_golang
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 22:35:37 +00:00
3eb8343ade Merge pull request #254 from tarampampam/dependabot/go_modules/github.com/urfave/cli/v2-2.27.1
build(deps): bump github.com/urfave/cli/v2 from 2.25.7 to 2.27.1
2024-01-01 22:34:59 +00:00
71b50da264 build(deps): bump github.com/urfave/cli/v2 from 2.25.7 to 2.27.1
Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.25.7 to 2.27.1.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.25.7...v2.27.1)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 22:34:46 +00:00
1354854ba9 Merge pull request #250 from tarampampam/dependabot/github_actions/aquasecurity/trivy-action-0.16.0
build(deps): bump aquasecurity/trivy-action from 0.14.0 to 0.16.0
2024-01-01 22:25:11 +00:00
1c91c2b2fa build(deps): bump aquasecurity/trivy-action from 0.14.0 to 0.16.0
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.14.0 to 0.16.0.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.14.0...0.16.0)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 22:24:59 +00:00
c289f2cb97 Merge pull request #248 from tarampampam/dependabot/go_modules/github.com/fatih/color-1.16.0
build(deps): bump github.com/fatih/color from 1.15.0 to 1.16.0
2023-12-01 22:36:28 +00:00
29771b9188 build(deps): bump github.com/fatih/color from 1.15.0 to 1.16.0
Bumps [github.com/fatih/color](https://github.com/fatih/color) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/fatih/color/releases)
- [Commits](https://github.com/fatih/color/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: github.com/fatih/color
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-01 22:36:13 +00:00
5deada7d6b Merge pull request #249 from tarampampam/dependabot/go_modules/github.com/valyala/fasthttp-1.51.0
build(deps): bump github.com/valyala/fasthttp from 1.50.0 to 1.51.0
2023-12-01 22:35:26 +00:00
99694d43e1 Merge pull request #247 from tarampampam/dependabot/go_modules/github.com/fasthttp/router-1.4.22
build(deps): bump github.com/fasthttp/router from 1.4.21 to 1.4.22
2023-12-01 22:35:16 +00:00
0d2418f8aa build(deps): bump github.com/valyala/fasthttp from 1.50.0 to 1.51.0
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.50.0 to 1.51.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.50.0...v1.51.0)

---
updated-dependencies:
- dependency-name: github.com/valyala/fasthttp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-01 22:35:14 +00:00
afdf0543b7 build(deps): bump github.com/fasthttp/router from 1.4.21 to 1.4.22
Bumps [github.com/fasthttp/router](https://github.com/fasthttp/router) from 1.4.21 to 1.4.22.
- [Release notes](https://github.com/fasthttp/router/releases)
- [Commits](https://github.com/fasthttp/router/compare/v1.4.21...v1.4.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-01 22:35:02 +00:00
605d019a3f Merge pull request #246 from tarampampam/dependabot/github_actions/aquasecurity/trivy-action-0.14.0
build(deps): bump aquasecurity/trivy-action from 0.13.1 to 0.14.0
2023-12-01 22:19:04 +00:00
3c1f5d9a99 build(deps): bump aquasecurity/trivy-action from 0.13.1 to 0.14.0
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.13.1 to 0.14.0.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.13.1...0.14.0)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-01 22:18:48 +00:00
8b18d02666 Update CHANGELOG.md 2023-11-20 12:28:22 +04:00
8e7570eee3 fix: "Too big request header" (#244)
* Added environment variable and CLI parameter for adjusting the read buffer size

* Changed readBufferSize to int and some bugfixing

* Small fixes

* ci: 👷 CI system updated

* ci: 👷 CI system updated

---------

Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2023-11-20 12:27:40 +04:00
2c8ba9c0f3 build(deps): bump github.com/prometheus/client_model (#241) 2023-11-02 05:14:07 +00:00
1ab0973011 build(deps): bump github.com/fasthttp/router from 1.4.20 to 1.4.21 (#242) 2023-11-02 05:13:41 +00:00
540139db3a chore(deps): update golangci/golangci-lint docker tag to v1.55 (#239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-02 09:13:02 +04:00
ff3d16d294 Merge pull request #243 from tarampampam/dependabot/github_actions/aquasecurity/trivy-action-0.13.1
build(deps): bump aquasecurity/trivy-action from 0.12.0 to 0.13.1
2023-11-01 22:55:13 +00:00
ebbded51bf build(deps): bump aquasecurity/trivy-action from 0.12.0 to 0.13.1
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.12.0 to 0.13.1.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/0.12.0...0.13.1)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 22:54:59 +00:00
384e45ce7f Merge pull request #240 from tarampampam/dependabot/go_modules/go.uber.org/goleak-1.3.0
build(deps): bump go.uber.org/goleak from 1.2.1 to 1.3.0
2023-11-01 22:19:14 +00:00
cdac8665de build(deps): bump go.uber.org/goleak from 1.2.1 to 1.3.0
Bumps [go.uber.org/goleak](https://github.com/uber-go/goleak) from 1.2.1 to 1.3.0.
- [Release notes](https://github.com/uber-go/goleak/releases)
- [Changelog](https://github.com/uber-go/goleak/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/goleak/compare/v1.2.1...v1.3.0)

---
updated-dependencies:
- dependency-name: go.uber.org/goleak
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-01 22:18:58 +00:00
fac512bd74 Fix spanish translated tags (#237) 2023-10-10 15:10:11 +04:00
ea85191d9e build(deps): bump actions/checkout from 3 to 4 (#232) 2023-10-09 12:46:36 +00:00
38ce1f9cf3 build(deps): bump docker/build-push-action from 4 to 5 (#234) 2023-10-09 12:46:25 +00:00
9e4a1451f5 build(deps): bump docker/setup-qemu-action from 2 to 3 (#235) 2023-10-09 12:42:41 +00:00
9feca1f509 Update dependabot.yml 2023-10-05 11:15:01 +04:00
38bf4abc1e Update dependabot.yml 2023-10-02 12:30:56 +04:00
ffc2af1c27 Update dependabot.yml 2023-10-02 11:35:41 +04:00
763c4ad109 build(deps): bump github.com/valyala/fasthttp from 1.49.0 to 1.50.0 (#227) 2023-10-02 07:07:00 +00:00
dcfd8ab3a7 build(deps): bump go.uber.org/zap from 1.25.0 to 1.26.0 (#229) 2023-10-02 06:54:19 +00:00
62f309cefd build(deps): bump docker/login-action from 2 to 3 (#233) 2023-10-02 06:53:40 +00:00
b5504de6d8 build(deps): bump github.com/prometheus/client_golang (#228) 2023-10-02 06:49:50 +00:00
4d91e17273 build(deps): bump aquasecurity/trivy-action from 0.11.2 to 0.12.0 (#230) 2023-10-02 06:49:03 +00:00
86e182c25d build(deps): bump docker/setup-buildx-action from 2 to 3 (#231) 2023-10-02 06:48:39 +00:00
8006cce4b0 Polish translation added. (#226)
* Polish translation added.

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Pаramtamtām <7326800+tarampampam@users.noreply.github.com>
2023-09-28 10:51:55 +04:00
caf4e33193 Update l10n.js (#225) 2023-09-24 21:11:49 -07:00
7ab0fa6f23 chore: The docker environment is refactored 2023-09-03 20:17:07 +04:00
81570b42c0 chore: The docker environment is refactored 2023-09-03 20:16:50 +04:00
4c3ebc055d Update CHANGELOG.md 2023-09-02 07:17:47 -07:00
61c1958717 build(deps): bump golang from 1.20-alpine to 1.21-alpine (#222)
* build(deps): bump golang from 1.20-alpine to 1.21-alpine

Bumps golang from 1.20-alpine to 1.21-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
...

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

* wip: 🔕 temporary commit

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2023-09-02 07:17:15 -07:00
3ab1a23ac5 Create dependabot.yml 2023-09-02 06:46:02 -07:00
a81c780e1e build(deps): bump go.uber.org/zap from 1.24.0 to 1.25.0 (#221) 2023-09-02 12:59:52 +00:00
2baeb2eb5b build(deps): bump github.com/valyala/fasthttp from 1.48.0 to 1.49.0 (#220) 2023-09-02 12:54:58 +00:00
e6b011b41b Add Indonesian translation (#218)
* Add Indonesian

Add all strings in bahasa indonesia.

* Update README.md

Add Indonesian flag emoji

* Add Indonesian language credit

* Update CHANGELOG.md (add 🇮🇩  language)

* Update l10n/l10n.js

---------

Co-authored-by: Pаramtamtām <7326800+tarampampam@users.noreply.github.com>
2023-09-01 10:25:17 +04:00
308467006b Add CatchAll functionality (#217)
* Add CatchAll functionality

* Added link to PR to changelog
2023-09-01 10:17:11 +04:00
ecf1359336 chore(deps): update golangci/golangci-lint docker tag to v1.54 (#215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-09 21:09:41 +04:00
a12dc4882e build(deps): bump github.com/fasthttp/router from 1.4.19 to 1.4.20 (#213) 2023-08-02 06:39:59 +00:00
980d0a5810 build(deps): bump go.uber.org/automaxprocs from 1.5.2 to 1.5.3 (#214) 2023-08-02 06:38:58 +00:00
eb3d84ee9d Change Ukrainian translation (#211)
* Update l10n.js

* Update l10n.js
2023-07-24 17:16:52 +04:00
6b43057333 build(deps): bump github.com/valyala/fasthttp from 1.47.0 to 1.48.0 (#208) 2023-07-01 23:48:11 +00:00
071ded0eac build(deps): bump github.com/prometheus/client_golang (#210) 2023-07-01 23:47:44 +00:00
47c4338c9e build(deps): bump aquasecurity/trivy-action from 0.10.0 to 0.11.2 (#207) 2023-07-01 23:43:11 +00:00
dfdeea4b6c build(deps): bump github.com/urfave/cli/v2 from 2.25.5 to 2.25.7 (#209) 2023-07-01 23:42:31 +00:00
cbb7936149 chore(deps): update golangci/golangci-lint docker tag to v1.53 (#200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-04 01:32:57 -07:00
c08e1307e8 build(deps): bump github.com/urfave/cli/v2 from 2.25.3 to 2.25.5 (#201) 2023-06-04 08:29:11 +00:00
25b86a057f build(deps): bump github.com/prometheus/client_model from 0.3.0 to 0.4.0 (#203) 2023-06-04 08:24:31 +00:00
ef72fa405d build(deps): bump github.com/fasthttp/router from 1.4.18 to 1.4.19 (#202) 2023-06-04 08:24:25 +00:00
1eb773fb57 depguard disabled 2023-06-04 01:18:45 -07:00
439b6d0326 build(deps): bump github.com/prometheus/client_golang (#199) 2023-06-02 06:35:01 +00:00
ef0081f711 build(deps): bump github.com/stretchr/testify from 1.8.2 to 1.8.4 (#198) 2023-06-02 06:34:58 +00:00
8c7a24b3d7 build(deps): bump github.com/urfave/cli/v2 from 2.25.1 to 2.25.3 (#197) 2023-05-04 11:20:25 +00:00
c76026c9f1 build(deps): bump github.com/prometheus/client_golang (#196) 2023-05-02 03:46:37 +00:00
37e4ecbf47 build(deps): bump aquasecurity/trivy-action from 0.9.2 to 0.10.0 (#194) 2023-05-02 03:46:24 +00:00
58dc38f72e build(deps): bump github.com/valyala/fasthttp from 1.45.0 to 1.47.0 (#195) 2023-05-02 03:46:09 +00:00
36c5472987 feat: IPv6 support (#192)
* 🐛 fix(server.go): validate IP address before starting server
 feat(server.go): add support for IPv6 addresses

*  feat(cli): add support for IPv6 addresses in the `--listen` flag

* 🐛 fix(server.go): add nolint comment to ignore magic number warning in ipv6 check

* 🐛 fix(server.go): use fmt.Sprintf to format IP and port instead of strconv.Itoa and string concatenation
2023-04-21 16:33:33 +04:00
717542e045 Update README.md 2023-04-18 10:48:55 +04:00
940bd0405f New template orient added (#190)
* added orient theme

* added creation test for orient theme

* Added new Orient theme to CHANGELOG.md

* fix: Template fixed a bit

---------

Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2023-04-17 15:06:28 +04:00
d40e8879d1 feat: Non-existing pages now return styled 404 status page (with 404 status code) (#189)
* 🎨 style(CHANGELOG.md): add UNRELEASED section
🔧 chore(notfound/handler.go): refactor to use core.RespondWithErrorPage
🔧 chore(http/server.go): pass config, templatePicker, renderer and options to notfoundHandler
🔧 chore(hurl/404.hurl): update test to expect HTML response instead of plain text

* 📝 docs(CHANGELOG.md): fix typo in changed section

* 🔥 chore(CHANGELOG.md): release version 2.22.0
🔥 chore(flags.go): remove unused code for serve command
2023-04-07 14:42:00 +04:00
6a67510bdc build(deps): bump go.uber.org/automaxprocs from 1.5.1 to 1.5.2 (#183) 2023-04-02 02:27:23 +00:00
a79521a37d build(deps): bump github.com/urfave/cli/v2 from 2.24.4 to 2.25.1 (#185) 2023-04-02 02:24:01 +00:00
4667194271 build(deps): bump github.com/fasthttp/router from 1.4.17 to 1.4.18 (#184) 2023-04-02 02:23:55 +00:00
a20852b03a build(deps): bump aquasecurity/trivy-action from 0.9.1 to 0.9.2 (#182) 2023-04-02 02:22:48 +00:00
ec0f1cc0d3 build(deps): bump github.com/valyala/fasthttp from 1.44.0 to 1.45.0 (#186) 2023-04-02 02:19:54 +00:00
49d9650d35 build(deps): bump github.com/fatih/color from 1.14.1 to 1.15.0 (#187) 2023-04-02 02:19:27 +00:00
1b876c45cc chore(deps): update golangci/golangci-lint docker tag to v1.52 (#181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-03-18 17:17:13 +04:00
23f52f25e2 Bump aquasecurity/trivy-action from 0.8.0 to 0.9.1 (#177) 2023-03-02 03:53:22 +00:00
16d7d80183 Bump github.com/stretchr/testify from 1.8.1 to 1.8.2 (#178) 2023-03-02 03:49:08 +00:00
5389fe00dd Bump github.com/fasthttp/router from 1.4.16 to 1.4.17 (#179) 2023-03-02 03:48:51 +00:00
cd67674976 Bump go.uber.org/goleak from 1.2.0 to 1.2.1 (#180) 2023-03-02 03:48:33 +00:00
36673a49a4 ci: 👷 CI system updated 2023-02-23 22:03:32 +04:00
b84b3ba9f4 chore: 🔧 Changes that do not modify src or test files 2023-02-23 21:50:35 +04:00
5343d8c934 docs(changelog): 📚 Changelog file updated 2023-02-23 21:50:31 +04:00
80891b0b46 ci: 👷 CI system updated 2023-02-23 21:50:20 +04:00
48313685ec chore: Module name changed, deps updated 2023-02-23 21:49:45 +04:00
7dc47c00ac chore(deps): update golangci/golangci-lint docker tag to v1.51 (#174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-02 17:08:43 +04:00
830b5bb934 chore(deps): update golang docker tag to v1.20 (#173)
* chore(deps): update golang docker tag to v1.20

* updated

* update

* update

* Update .github/workflows/tests.yml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2023-02-02 13:09:29 +04:00
8d43984644 Bump github.com/urfave/cli/v2 from 2.24.1 to 2.24.2 (#168) 2023-02-02 07:25:44 +00:00
1b152c1a80 Bump github.com/fasthttp/router from 1.4.14 to 1.4.16 (#169) 2023-02-02 03:26:30 +00:00
175b9f0cfb Bump go.uber.org/goleak from 1.1.11 to 1.2.0 (#170) 2023-02-02 03:25:57 +00:00
16180be7e2 Bump github.com/valyala/fasthttp from 1.43.0 to 1.44.0 (#171) 2023-02-02 03:22:36 +00:00
11b270ee68 Bump github.com/fatih/color from 1.13.0 to 1.14.1 (#172) 2023-02-02 03:22:15 +00:00
97ab8a4475 Bump docker/build-push-action from 3 to 4 (#167) 2023-02-02 03:20:00 +00:00
da3b864e02 fix: docker-compose server running command 2023-01-29 15:56:58 +04:00
0bd989e493 docs(changelog): file updated 2023-01-29 15:49:28 +04:00
59c4d2022c ci: workflow files have been updated 2023-01-29 15:39:49 +04:00
1ec17caa1d feat: Possibility to use custom env variables in templates (#165) 2023-01-29 15:25:38 +04:00
252618a975 chore: Better CLI (#163) 2023-01-29 14:54:56 +04:00
315c7660d1 Update README.md 2022-12-27 11:41:51 +04:00
6a6809b07f Bump go.uber.org/zap from 1.23.0 to 1.24.0 (#159) 2022-12-02 06:09:55 +00:00
a960b5928e Bump github.com/valyala/fasthttp from 1.41.0 to 1.43.0 (#157) 2022-12-02 06:06:16 +00:00
46d96d7bb4 Bump github.com/prometheus/client_golang from 1.13.0 to 1.14.0 (#160) 2022-12-02 06:02:10 +00:00
69063a9cf7 Bump github.com/fasthttp/router from 1.4.13 to 1.4.14 (#158) 2022-12-02 06:01:28 +00:00
7c8d2f54c7 Update orangeopensource/hurl Docker tag to v1.8.0 (#155)
* Update orangeopensource/hurl Docker tag to v1.8.0

* wip: temporary commit

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

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

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

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

* wip: temporary commit

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

* update readmes and changelog
2022-06-04 17:06:39 +05:00
a932f94ec0 Bump docker/setup-buildx-action from 1 to 2 (#111) 2022-06-02 05:14:32 +00:00
3ac2c74249 Bump github.com/valyala/fasthttp from 1.36.0 to 1.37.0 (#106) 2022-06-02 05:14:08 +00:00
18af96bada Bump github.com/prometheus/client_golang from 1.12.1 to 1.12.2 (#108) 2022-06-02 05:10:46 +00:00
445aad8b41 Bump docker/login-action from 1 to 2 (#109) 2022-06-02 05:10:27 +00:00
b61cc7460f Bump github.com/fasthttp/router from 1.4.8 to 1.4.9 (#110) 2022-06-02 05:10:14 +00:00
c9586fe79a Bump aquasecurity/trivy-action from 0.2.5 to 0.3.0 (#112) 2022-06-02 05:09:47 +00:00
405afec38a Bump docker/setup-qemu-action from 1 to 2 (#107) 2022-06-02 05:09:16 +00:00
5e0be010b7 Bump docker/build-push-action from 2 to 3 (#113) 2022-06-02 05:09:00 +00:00
9bc00fa4ca Update release.yml 2022-05-12 17:20:02 +05:00
6742381562 Update tests.yml 2022-05-12 17:18:45 +05:00
6d3ced480d Add 🇳🇱 Dutch translation (#104)
Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2022-05-07 15:09:23 +05:00
e8fa8896c9 Update CHANGELOG.md 2022-05-06 22:28:42 +05:00
c9bd47618d adding translation to Portuguese language (#103)
Co-authored-by: Fabio Correia <fabiocorreia@trixlog.com>
Co-authored-by: Paramtamtam <7326800+tarampampam@users.noreply.github.com>
2022-05-06 22:28:12 +05:00
3ffb952cdd Bump golang from 1.18.0-alpine to 1.18.1-alpine (#95) 2022-05-02 06:54:34 +00:00
b5892f44d9 Bump github.com/valyala/fasthttp from 1.34.0 to 1.36.0 (#96) 2022-05-02 04:24:25 +00:00
769b0cebb6 Bump github.com/fasthttp/router from 1.4.7 to 1.4.8 (#98) 2022-05-02 04:18:32 +00:00
8f49ff7204 Bump github/codeql-action from 1 to 2 (#99) 2022-05-02 04:18:12 +00:00
f89bdfbd51 Bump aquasecurity/trivy-action from 0.2.2 to 0.2.5 (#97) 2022-05-02 04:17:53 +00:00
1b2e899201 Bump actions/download-artifact from 2 to 3 (#100) 2022-05-02 04:17:31 +00:00
6c0c85544e Bump actions/upload-artifact from 2 to 3 (#101) 2022-05-02 04:17:11 +00:00
d3d1c62411 Bump codecov/codecov-action from 2 to 3 (#102) 2022-05-02 04:16:54 +00:00
180 changed files with 10231 additions and 8092 deletions

View File

@ -1,26 +0,0 @@
# Docs: <https://docs.codecov.io/docs/commit-status>
coverage:
# coverage lower than 50 is red, higher than 90 green
range: 30..80
status:
project:
default:
# Choose a minimum coverage ratio that the commit must meet to be considered a success.
#
# `auto` will use the coverage from the base commit (pull request base or parent commit) coverage to compare
# against.
target: auto
# Allow the coverage to drop by X%, and posting a success status.
threshold: 5%
# Resulting status will pass no matter what the coverage is or what other settings are specified.
informational: true
patch:
default:
target: auto
threshold: 5%
informational: true

View File

@ -1,14 +1,9 @@
.dockerignore
Dockerfile
.github
.git
.gitignore
.editorconfig
.idea
.vscode
test
temp
tmp
LICENSE
Makefile
error-pages
## Ignore everything
*
## Except the following files and directories
!/cmd
!/internal
!/l10n
!/templates
!/go.*

View File

@ -7,11 +7,12 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
indent_size = 2
trim_trailing_whitespace = true
[*.{yml, yaml, sh, conf}]
indent_size = 2
[{*.yml,*.yaml}]
ij_any_spaces_within_braces = false
ij_any_spaces_within_brackets = false
[{Makefile, go.mod, *.go}]
[{Makefile,go.mod,*.go}]
indent_style = tab

9
.gitattributes vendored
View File

@ -1,9 +0,0 @@
# Text files have auto line endings
* text=auto
# Go source files always have LF line endings
*.go text eol=lf
# Disable next extensions in project "used languages" list
*.lua linguist-detectable=false
*.html linguist-detectable=false

View File

@ -1,4 +1,5 @@
# Docs: <https://git.io/JR5E4>
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
# docs: https://git.io/JR5E4
name: 🐞 Bug report
description: File a bug/issue
@ -35,7 +36,9 @@ body:
id: configs
attributes:
label: Configuration files
description: Please copy and paste any relevant configuration files. This will be automatically formatted into code (yaml), so no need for backticks.
description: |
Please copy and paste any relevant configuration files. This will be automatically formatted
into code (yaml), so no need for backticks.
render: yaml
placeholder: Traefik, docker-compose, helm, etc.
@ -43,11 +46,12 @@ body:
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code (shell), so no need for backticks.
description: |
Please copy and paste any relevant log output. This will be automatically formatted into code
(shell), so no need for backticks.
render: shell
- type: textarea
attributes:
label: Anything else?
description: Links? References? Anything that will give us more context about the issue you are encountering!
placeholder: You can attach images or log files by clicking this area to highlight it and then dragging files in

View File

@ -1,4 +1,5 @@
# Docs: <https://git.io/JP3tm>
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
# docs: https://git.io/JP3tm
blank_issues_enabled: false

View File

@ -1,4 +1,5 @@
# Docs: <https://git.io/JR5E4>
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
# docs: https://git.io/JR5E4
name: 💡 Feature request
description: Suggest an idea for this project

View File

@ -1,22 +1,23 @@
# Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates>
# yaml-language-server: $schema=https://json.schemastore.org/dependabot-2.0.json
# docs: https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates
version: 2
updates:
- package-ecosystem: gomod
directory: /
groups: {gomod: {patterns: ['*']}}
schedule: {interval: monthly}
reviewers: [tarampampam]
assignees: [tarampampam]
- package-ecosystem: github-actions
directory: /
groups: {github-actions: {patterns: ['*']}}
schedule: {interval: monthly}
reviewers: [tarampampam]
assignees: [tarampampam]
- package-ecosystem: docker
directory: /
groups: {docker: {patterns: ['*']}}
schedule: {interval: monthly}
reviewers: [tarampampam]
assignees: [tarampampam]

13
.github/release.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-release-config.json
# docs: https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
changelog:
categories:
- title: 🛠 Fixes
labels: [type:fix, type:bug]
- title: 🚀 Features
labels: [type:feature, type:feature_request]
- title: 📦 Dependency updates
labels: [dependencies]
- title: Other Changes
labels: ['*']

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

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

27
.github/workflows/dependabot.yml vendored Normal file
View File

@ -0,0 +1,27 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# docs: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
name: 🤖 Dependabot
on:
pull_request: {}
permissions:
contents: write
pull-requests: write
jobs:
dependabot: # https://tinyurl.com/e69djmen
name: Enable auto-merge for Dependabot PRs
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- uses: dependabot/fetch-metadata@v2
id: metadata
with: {github-token: "${{ secrets.GITHUB_TOKEN }}"}
- if: ${{ contains(fromJSON('["version-update:semver-minor", "version-update:semver-patch"]'), steps.metadata.outputs.update-type) }}
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,4 +1,7 @@
name: documentation
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# docs: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
name: 📚 Documentation
on:
push:
@ -8,11 +11,11 @@ on:
jobs:
docker-hub-description:
name: Docker Hub Description
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: peter-evans/dockerhub-description@v3 # Action page: <https://github.com/peter-evans/dockerhub-description>
- uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_USER_PASSWORD }}

View File

@ -1,116 +1,95 @@
name: release
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# docs: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
name: 🚀 Release
on:
release: # Docs: <https://git.io/JeBz1#release-event-release>
types: [published]
jobs:
purge-cdn-cache:
name: Purge jsDelivr CDN cache
runs-on: ubuntu-20.04
steps:
- uses: gacts/purge-jsdelivr-cache@v1 # Action page: <https://github.com/gacts/purge-jsdelivr-cache>
with:
url: |
https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.js
https://cdn.jsdelivr.net/gh/tarampampam/error-pages@2/l10n/l10n.min.js
https://cdn.jsdelivr.net/gh/tarampampam/error-pages@latest/l10n/l10n.js
https://cdn.jsdelivr.net/gh/tarampampam/error-pages@latest/l10n/l10n.min.js
build:
name: Build for ${{ matrix.os }} (${{ matrix.arch }})
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [linux, darwin] # linux, freebsd, darwin, windows
arch: [amd64] # amd64, 386
os: [linux, darwin, windows] # freebsd
arch: [amd64, arm64] # 386
steps:
- uses: actions/setup-go@v3
with: {go-version: 1.18.0}
- uses: actions/checkout@v3
- uses: gacts/github-slug@v1
id: slug
- name: Generate builder values
id: values
run: echo "::set-output name=binary-name::error-pages-${{ matrix.os }}-${{ matrix.arch }}"
- name: Build application
env:
- uses: actions/checkout@v4
- {uses: actions/setup-go@v5, with: {go-version-file: go.mod}}
- {uses: gacts/github-slug@v1, id: slug}
- id: values
run: echo "binary-name=error-pages-${{ matrix.os }}-${{ matrix.arch }}`[ ${{ matrix.os }} = 'windows' ] && echo '.exe'`" >> $GITHUB_OUTPUT
- env:
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
CGO_ENABLED: 0
LDFLAGS: -s -w -X github.com/tarampampam/error-pages/internal/version.version=${{ steps.slug.outputs.version }}
LDFLAGS: -s -w -X gh.tarampamp.am/error-pages/internal/version.version=${{ steps.slug.outputs.version }}
run: go build -trimpath -ldflags "$LDFLAGS" -o "./${{ steps.values.outputs.binary-name }}" ./cmd/error-pages/
- name: Upload binary file to release
uses: svenstaro/upload-release-action@v2
- uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ steps.values.outputs.binary-name }}
asset_name: ${{ steps.values.outputs.binary-name }}
tag: ${{ github.ref }}
docker-image:
name: Build docker image
runs-on: ubuntu-20.04
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
run: mkdir ./out && ./${{ steps.values.outputs.binary-name }} build --index --target-dir ./out
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
uses: actions/upload-artifact@v4
with:
name: error-pages-static
path: out/
if-no-files-found: error
retention-days: 1
demo:
name: Update the demo (GitHub Pages)
runs-on: ubuntu-latest
needs: [build]
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v4
with:
name: error-pages-static
path: .artifact
- uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./.artifact
- uses: gacts/github-slug@v1
id: slug
- uses: docker/setup-qemu-action@v1 # Action page: <https://github.com/docker/setup-qemu-action>
- uses: docker/setup-buildx-action@v1 # Action page: <https://github.com/docker/setup-buildx-action>
- uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
docker-image:
name: Build the docker image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- {uses: gacts/github-slug@v1, id: slug}
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_PASSWORD }}
- uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_PASSWORD }}
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action>
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8
build-args: "APP_VERSION=${{ steps.slug.outputs.version }}"
tags: |
tarampampam/error-pages:${{ steps.slug.outputs.version }}
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
tarampampam/error-pages:${{ steps.slug.outputs.version-major }}
tarampampam/error-pages:latest
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
demo:
name: Update the demonstration
runs-on: ubuntu-20.04
needs: [docker-image]
steps:
- uses: gacts/github-slug@v1
id: slug
- name: Take rendered templates from the built docker image
run: |
docker create --name img ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
docker cp img:/opt/html ./out
docker rm -f img
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./out
tags: ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
# tags: | # TODO: uncomment after the stable release
# tarampampam/error-pages:latest
# tarampampam/error-pages:${{ steps.slug.outputs.version }}
# tarampampam/error-pages:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
# tarampampam/error-pages:${{ steps.slug.outputs.version-major }}
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}.${{ steps.slug.outputs.version-minor }}
# ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version-major }}

View File

@ -1,4 +1,7 @@
name: tests
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# docs: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
name: 🧪 Tests
on:
push:
@ -8,163 +11,62 @@ on:
pull_request:
paths-ignore: ['**.md']
jobs: # Docs: <https://git.io/JvxXE>
gitleaks:
name: Gitleaks
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with: {fetch-depth: 0}
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
- uses: zricethezav/gitleaks-action@v1 # Action page: <https://github.com/zricethezav/gitleaks-action>
jobs:
gitleaks:
name: Check for GitLeaks
runs-on: ubuntu-latest
steps:
- {uses: actions/checkout@v4, with: {fetch-depth: 0}}
- uses: gacts/gitleaks@v1
golangci-lint:
name: Golang-CI (lint)
runs-on: ubuntu-20.04
name: Run golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with: {go-version: 1.17} # On v1.18 I had an error "panic: load embedded ruleguard rules: rules/rules.go:13: can't load fmt"
- name: Run linter
uses: golangci/golangci-lint-action@v3 # Action page: <https://github.com/golangci/golangci-lint-action>
with:
version: v1.44 # without patch version
only-new-issues: false # show only new issues if it's a pull request
validate-config-file:
name: Validate config file
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with: {node-version: '16'}
- name: Install linter
run: npm install -g ajv-cli # Package page: <https://www.npmjs.com/package/ajv-cli>
- name: Run linter
run: ajv validate --all-errors --verbose -s ./schemas/config/1.0.schema.json -d ./error-pages.y*ml
lint-l10n:
name: Lint l10n file(s)
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with: {node-version: '16'}
- name: Install eslint
run: npm install -g eslint@v8 # Package page: <https://www.npmjs.com/package/eslint>
- name: Run linter
working-directory: l10n
run: eslint ./*.js
- uses: actions/checkout@v4
- {uses: actions/setup-go@v5, with: {go-version-file: go.mod}}
- uses: golangci/golangci-lint-action@v6
go-test:
name: Unit tests
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with: {go-version: 1.18}
- uses: actions/checkout@v3
with: {fetch-depth: 2} # Fixes codecov error 'Issue detecting commit SHA'
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
uses: actions/cache@v3
id: go-cache
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- if: steps.go-cache.outputs.cache-hit != 'true'
run: go mod download
- name: Run Unit tests
run: go test -race -covermode=atomic -coverprofile /tmp/coverage.txt ./...
- uses: codecov/codecov-action@v2 # https://github.com/codecov/codecov-action
continue-on-error: true
with:
file: /tmp/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
- uses: actions/checkout@v4
- {uses: actions/setup-go@v5, with: {go-version-file: go.mod}}
- run: go test -race ./...
build:
name: Build for ${{ matrix.os }} (${{ matrix.arch }})
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [linux, darwin] # linux, freebsd, darwin, windows
arch: [amd64] # amd64, 386
needs: [golangci-lint, go-test, validate-config-file]
os: [linux, darwin, windows] # freebsd
arch: [amd64, arm64] # 386
needs: [golangci-lint, go-test]
steps:
- uses: actions/setup-go@v3
with: {go-version: 1.18}
- uses: actions/checkout@v3
- uses: gacts/github-slug@v1
id: slug
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
uses: actions/cache@v3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- run: go mod download
- name: Build application
env:
- uses: actions/checkout@v4
- {uses: actions/setup-go@v5, with: {go-version-file: go.mod}}
- {uses: gacts/github-slug@v1, id: slug}
- env:
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
CGO_ENABLED: 0
LDFLAGS: -s -w -X github.com/tarampampam/error-pages/internal/version.version=${{ steps.slug.outputs.branch-name-slug }}@${{ steps.slug.outputs.commit-hash-short }}
LDFLAGS: -s -w -X gh.tarampamp.am/error-pages/internal/appmeta.version=${{ steps.slug.outputs.commit-hash-short }}
run: go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/
- name: Try to execute
if: matrix.os == 'linux'
run: ./error-pages version && ./error-pages -h
- uses: actions/upload-artifact@v2
with:
name: error-pages-${{ matrix.os }}-${{ matrix.arch }}
path: error-pages
if-no-files-found: error
retention-days: 1
generate:
name: Run templates generator
runs-on: ubuntu-20.04
needs: [build]
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v2
with:
name: error-pages-linux-amd64
path: .artifact
- name: Prepare binary file to run
working-directory: .artifact
run: mv ./error-pages ./../error-pages && chmod +x ./../error-pages
- name: Run generator
run: ./error-pages build ./out --verbose --index
- name: Test files creation
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
run: ./error-pages --version && ./error-pages -h
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
run: mkdir ./out && ./error-pages --log-level=debug build --index --target-dir ./out
- if: matrix.os == 'linux' && matrix.arch == 'amd64'
run: |
test -f ./out/index.html
test -f ./out/ghost/404.html
test -f ./out/l7-dark/404.html
test -f ./out/l7-light/404.html
test -f ./out/l7/404.html
test -f ./out/shuffle/404.html
test -f ./out/noise/404.html
test -f ./out/hacker-terminal/404.html
@ -172,91 +74,19 @@ jobs: # Docs: <https://git.io/JvxXE>
test -f ./out/lost-in-space/404.html
test -f ./out/app-down/404.html
test -f ./out/connection/404.html
test -f ./out/matrix/404.html
test -f ./out/orient/404.html
docker-image:
name: Build docker image
runs-on: ubuntu-20.04
needs: [golangci-lint, go-test, validate-config-file]
name: Build the docker image
runs-on: ubuntu-latest
needs: [golangci-lint, go-test]
steps:
- uses: actions/checkout@v3
- uses: gacts/github-slug@v1
id: slug
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action>
- uses: actions/checkout@v4
- {uses: gacts/github-slug@v1, id: slug}
- uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: false
build-args: "APP_VERSION=${{ steps.slug.outputs.branch-name-slug }}@${{ steps.slug.outputs.commit-hash-short }}"
build-args: "APP_VERSION=${{ steps.slug.outputs.commit-hash-short }}"
tags: app:ci
- run: docker save app:ci > ./docker-image.tar
- uses: actions/upload-artifact@v2
with:
name: docker-image
path: ./docker-image.tar
retention-days: 1
scan-docker-image:
name: Scan the docker image
runs-on: ubuntu-20.04
needs: [docker-image]
steps:
- uses: actions/checkout@v3 # is needed for `upload-sarif` action
- uses: actions/download-artifact@v2
with:
name: docker-image
path: .artifact
- uses: aquasecurity/trivy-action@0.2.2 # action page: <https://github.com/aquasecurity/trivy-action>
with:
input: .artifact/docker-image.tar
format: sarif
severity: MEDIUM,HIGH,CRITICAL
exit-code: 1
output: trivy-results.sarif
- uses: github/codeql-action/upload-sarif@v1
if: always()
continue-on-error: true
with: {sarif_file: trivy-results.sarif}
poke-docker-image:
name: Run the docker image
runs-on: ubuntu-20.04
needs: [docker-image]
timeout-minutes: 2
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v2
with:
name: docker-image
path: .artifact
- working-directory: .artifact
run: docker load < docker-image.tar
- name: Download hurl
env:
VERSION: 1.5.0
run: curl -SL -o hurl.deb https://github.com/Orange-OpenSource/hurl/releases/download/${VERSION}/hurl_${VERSION}_amd64.deb
- name: Install hurl
run: sudo dpkg -i hurl.deb
- name: Run container with the app
run: docker run --rm -d -p "8080:8080/tcp" -e "SHOW_DETAILS=true" -e "PROXY_HTTP_HEADERS=X-Foo,Bar,Baz_blah" --name app app:ci
- name: Wait for container "healthy" state
run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done
- run: hurl --color --test --fail-at-end --variable host=127.0.0.1 --variable port=8080 --summary ./test/hurl/*.hurl
- name: Stop the container
if: always()
run: docker kill app

7
.gitignore vendored
View File

@ -8,9 +8,14 @@
## Temp dirs & trash
/temp
/tmp
*.env
/*-old
/cmd/test*
.DS_Store
/go.work*
*.cache
*.out
*.env
/out
/gen
/cover*.*
/report.xml

View File

@ -1,26 +1,32 @@
# Documentation: <https://github.com/golangci/golangci-lint#config-file>
# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json
# docs: https://github.com/golangci/golangci-lint#config-file
run:
timeout: 1m
skip-dirs:
- .github
- .git
- tmp
- temp
timeout: 2m
modules-download-mode: readonly
allow-parallel-runners: true
output:
format: colored-line-number # colored-line-number|line-number|json|tab|checkstyle|code-climate
formats: [{format: colored-line-number}] # colored-line-number|line-number|json|tab|checkstyle|code-climate
linters-settings:
gci:
sections:
- standard
- default
- prefix(gh.tarampamp.am/error-pages)
gofmt:
simplify: false
rewrite-rules:
- { pattern: 'interface{}', replacement: 'any' }
govet:
check-shadowing: true
enable:
- shadow
gocyclo:
min-complexity: 15
godot:
scope: declarations
capital: true
capital: false
dupl:
threshold: 100
goconst:
@ -28,25 +34,28 @@ linters-settings:
min-occurrences: 3
misspell:
locale: US
ignore-words: [cancelled]
lll:
line-length: 120
forbidigo:
forbid:
- '^(fmt\.Print(|f|ln)|print(|ln))(# it looks like a forgotten debugging printing call)?$'
prealloc:
simple: true
range-loops: true
for-loops: true
nolintlint:
allow-leading-space: false
require-specific: true
nakedret:
# Make an issue if func has more lines of code than this setting, and it has naked returns.
# Default: 30
max-func-lines: 100
linters: # All available linters list: <https://golangci-lint.run/usage/linters/>
disable-all: true
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bidichk # Checks for dangerous unicode character sequences
- bodyclose # Checks whether HTTP response body is closed successfully
- contextcheck # check the function whether use a non-inherited context
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
@ -54,47 +63,65 @@ linters: # All available linters list: <https://golangci-lint.run/usage/linters/
- exhaustive # check exhaustiveness of enum switch statements
- exportloopref # checks for pointers to enclosing loop variables
- funlen # Tool for detection of long functions
- gci # Gci control golang package import order and make it always deterministic
- godot # Check if comments end in a period
- gochecknoglobals # Checks that no globals are present in Go code
- gochecknoinits # Checks that no init functions are present in Go code
- gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- gocyclo # Computes and checks the cyclomatic complexity of functions
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
- gomnd # An analyzer to detect magic numbers
- gofmt # Gofmt checks whether code was gofmt-ed. By default, this tool runs with -s option to check for code simplification
- goimports # Goimports does everything that gofmt does. Additionally, it checks unused imports
- mnd # An analyzer to detect magic numbers
- goprintffuncname # Checks that printf-like functions are named with `f` at the end
- gosec # Inspects source code for security problems
- gosimple # Linter for Go source code that specializes in simplifying a code
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- ineffassign # Detects when assignments to existing variables are not used
- lll # Reports long lines
- forbidigo # Forbids identifiers
- misspell # Finds commonly misspelled English words in comments
- nakedret # Finds naked returns in functions greater than a specified function length
- nestif # Reports deeply nested if statements
- nlreturn # checks for a new line before return and branch statements to increase code clarity
- noctx # finds sending http request without context.Context
- nolintlint # Reports ill-formed or insufficient nolint directives
- prealloc # Finds slice declarations that could potentially be preallocated
- rowserrcheck # Checks whether Err of rows is checked successfully
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
- structcheck # Finds unused struct fields
- stylecheck # Stylecheck is a replacement for golint
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
- promlinter # Check Prometheus metrics naming via promlint.
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types
- varcheck # Finds unused global variables and constants
- whitespace # Tool for detection of leading and trailing whitespace
- wsl # Whitespace Linter - Forces you to use empty lines!
- unused # Checks Go code for unused constants, variables, functions and types
- gosimple # Linter for Go source code that specializes in simplifying code
- staticcheck # It's a set of rules from staticcheck
- asasalint # Check for pass []any as any in variadic func(...any)
- bodyclose # Checks whether HTTP response body is closed successfully
- contextcheck # Check whether the function uses a non-inherited context
- decorder # Check declaration order and count of types, constants, variables and functions
- dupword # Checks for duplicate words in the source code
- durationcheck # Check for two durations multiplied together
- errchkjson # Checks types passed to the json encoding functions
- errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
issues:
exclude-dirs:
- .github
- .git
- tmp
- temp
- testdata
exclude-rules:
- {path: flags\.go, linters: [gochecknoglobals, lll, mnd, dupl]}
- {path: env\.go, linters: [lll, gosec]}
- path: _test\.go
linters:
- dupl
- dupword
- lll
- nolintlint
- funlen
- scopelint
- gocognit
- noctx
- goconst
- nlreturn
- gochecknoglobals

View File

@ -1,327 +0,0 @@
# Changelog
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.13.0
### Added
- Possibility to disable error pages auto-localization (using `--disable-l10n` flag for the `serve` & `build` commands or environment variable `DISABLE_L10N`) [#91]
### Fixed
- User UID/GID changed to the numeric values in the dockerfile [#92]
[#92]:https://github.com/tarampampam/error-pages/issues/92
[#91]:https://github.com/tarampampam/error-pages/issues/91
## v2.12.1
### Fixed
- Fix translation 🇫🇷 [#86]
[#85]:https://github.com/tarampampam/error-pages/pull/86
## v2.12.0
### Changed
- Error pages now translated in 🇫🇷 [#82]
[#82]:https://github.com/tarampampam/error-pages/pull/82
## v2.11.0
### Added
- Template `matrix` [#81]
### Fixed
- Localization mistakes [#81]
[#81]:https://github.com/tarampampam/error-pages/pull/81
## v2.10.1
### Fixed
- Template `shuffle`
- Localization mistakes
## v2.10.0
### Changed
- Error pages now translated in 🇺🇦 and 🇷🇺 languages [#80]
[#80]:https://github.com/tarampampam/error-pages/pull/80
## v2.9.0
### Added
- Template `connection` [#79]
[#79]:https://github.com/tarampampam/error-pages/pull/79
## v2.8.1
### Fixed
- Dark mode for `app-down` template
### Changed
- The index page for built error pages now supports a dark theme
## v2.8.0
### Added
- Template `app-down` [#74]
### Changed
- Go updated from `1.17.6` up to `1.18.0`
[#74]:https://github.com/tarampampam/error-pages/pull/74
## v2.7.0
### Changed
- Logs includes request/response headers now [#67]
### Added
- Possibility to proxy HTTP headers from the requests to the responses (can be enabled using `--proxy-headers` flag for the `serve` command or environment variable `PROXY_HTTP_HEADERS`, headers list should be comma-separated) [#67]
- Template `lost-in-space` [#68]
### Fixed
- Template `l7-light` uses the dark colors in the browsers with the preferred dark theme
[#67]:https://github.com/tarampampam/error-pages/pull/67
[#68]:https://github.com/tarampampam/error-pages/pull/68
## v2.6.0
### Added
- Possibility to change the template to the random once a day using "special" template name `random-daily` (or hourly, using `random-hourly`) [#48]
[#48]:https://github.com/tarampampam/error-pages/issues/48
## v2.5.0
### Changed
- Go updated from `1.17.5` up to `1.17.6`
### Added
- `Host` and `X-Forwarded-For` Header to error pages [#61]
### Fixed
- Performance issue, that affects template rendering. Now templates are cached in memory (for 2 seconds), and it has improved performance by more than 200% [#60]
[#60]:https://github.com/tarampampam/error-pages/pull/60
[#61]:https://github.com/tarampampam/error-pages/pull/61
## v2.4.0
### Changed
- It is now possible to use [golang-tags of templates](https://pkg.go.dev/text/template) in error page templates and formatted (`json`, `xml`) responses [#49]
- Health-check route become `/healthz` (instead `/health/live`, previous route marked as deprecated) [#49]
### Added
- The templates contain details block now (can be enabled using `--show-details` flag for the `serve` command or environment variable `SHOW_DETAILS=true`) [#49]
- Formatted response templates (`json`, `xml`) - the server responds with a formatted response depending on the `Content-Type` (and `X-Format`) request header value [#49]
- HTTP header `X-Robots-Tag: noindex` for the error pages [#49]
- Possibility to pass the needed error page code using `X-Code` HTTP header [#49]
- Possibility to integrate with [ingress-nginx](https://kubernetes.github.io/ingress-nginx/) [#49]
- Metrics HTTP endpoint `/metrics` in prometheus format [#54]
### Fixed
- Potential race condition (in the `pick.StringsSlice` struct) [#49]
[#54]:https://github.com/tarampampam/error-pages/pull/54
[#49]:https://github.com/tarampampam/error-pages/pull/49
## v2.3.0
### Added
- Flag `--default-http-code` for the `serve` subcommand (`404` is used by default instead of `200`, environment name `DEFAULT_HTTP_CODE`) [#41]
### Changed
- Go updated from `1.17.1` up to `1.17.5`
[#41]:https://github.com/tarampampam/error-pages/issues/41
## v2.2.0
### Added
- Template `cats` [#31]
[#31]:https://github.com/tarampampam/error-pages/pull/31
## v2.1.0
### Added
- `referer` field in access log records
- Flag `--default-error-page` for the `serve` subcommand (`404` is used by default, environment name `DEFAULT_ERROR_PAGE`)
### Changed
- The source code has been refactored
- The index page (`/`) now returns the error page with a code, declared using `--default-error-page` flag (HTTP code 200, when a page code exists)
## v2.0.0
### Changed
- Application rewritten in Go
## v1.8.0
### Added
- Nginx health-check endpoint (`/health/live`) and dockerfile `HEALTHCHECK` to utilise (thx [@modem7](https://github.com/modem7)) [#22], [#23]
[#22]:https://github.com/tarampampam/error-pages/pull/22
[#23]:https://github.com/tarampampam/error-pages/pull/23
## v1.7.2
### Changed
- Nginx updated up to `1.21` (from `1.19`)
## v1.7.1
### Fixed
- Random template selecting (thx [@xpliz](https://github.com/xpliz)) [#12]
[#12]:https://github.com/tarampampam/error-pages/pull/12
## v1.7.0
### Added
- Template `hacker-terminal` [#13]
- HTML comments with error code and description into each template (header and footer, it seems more readable for curl usage)
[#10]:https://github.com/tarampampam/error-pages/pull/13
## v1.6.0
### Added
- Template `noise` [#10]
### Fixed
- File permissions in docker image
[#10]:https://github.com/tarampampam/error-pages/issues/10
## v1.5.0
### Changed
- Repository files structure
- Nginx updated from `1.18` up to `1.19` in docker image
- Docker image now uses default `nginx` entrypoint scripts and command
### Added
- Support for `linux/arm64/v8`, `linux/arm/v6` and `linux/arm/v7` platforms for docker image
- Random template selecting (use `random` as a template name) for docker image
## v1.4.0
### Added
- Template `shuffle` [#4]
[#4]:https://github.com/tarampampam/error-pages/issues/4
## v1.3.1
### Fixed
- `can't create directory '/opt/html/nginx-error-pages'` error [#3]
[#3]:https://github.com/tarampampam/error-pages/issues/3
## v1.3.0
### Added
- `418` status code error page
- Set `server_tokens off;` in `nginx` server configuration
## v1.2.0
### Fixed
- By default `nginx` in docker container returns 404 http code instead 200 when `/` requested
### Changed
- Default value for `TEMPLATE_NAME` is `ghost` now
### Removed
- Environment variable `DEFAULT_ERROR_CODE` support in docker image
### Added
- Templates `l7-light` and `l7-dark`
## v1.1.0
### Added
- Environment variable `DEFAULT_ERROR_CODE` support in docker image
## v1.0.1
### Changed
- Repository (not docker image) renamed from `error-pages-docker` to `error-pages`
- `configuration.json` renamed to `config.json`
- Makefile contains new targets (`install`, `gen`, `preview`)
- Generator logging messages
### Added
- `docker-compose` for development
### Fixed
- Readme file content [#1]
[#1]:https://github.com/tarampampam/error-pages/issues/1
## v1.0.0
### Changed
- First project release
[keepachangelog]:https://keepachangelog.com/en/1.0.0/
[semver]:https://semver.org/spec/v2.0.0.html

View File

@ -1,53 +1,73 @@
# syntax=docker/dockerfile:1.2
# syntax=docker/dockerfile:1
# Image page: <https://hub.docker.com/_/golang>
FROM golang:1.18.0-alpine as builder
# -✂- this stage is used to develop and build the application locally -------------------------------------------------
FROM docker.io/library/golang:1.22-bookworm AS develop
# 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"
# use the /var/tmp as the GOPATH to reuse the modules cache
ENV GOPATH="/var/tmp/go"
RUN set -x \
# renovate: source=github-releases name=golangci/golangci-lint
&& GOLANGCI_LINT_VERSION="1.59.1" \
&& wget -O- -nv "https://cdn.jsdelivr.net/gh/golangci/golangci-lint@v${GOLANGCI_LINT_VERSION}/install.sh" \
| sh -s -- -b /bin "v${GOLANGCI_LINT_VERSION}"
RUN set -x \
# customize the shell prompt (for the bash)
&& echo "PS1='\[\033[1;36m\][go] \[\033[1;34m\]\w\[\033[0;35m\] \[\033[1;36m\]# \[\033[0m\]'" >> /etc/bash.bashrc
WORKDIR /src
COPY . .
# burn the modules cache
RUN \
--mount=type=bind,source=go.mod,target=/src/go.mod \
--mount=type=bind,source=go.sum,target=/src/go.sum \
go mod download -x \
&& find "${GOPATH}" -type d -exec chmod 0777 {} \; \
&& find "${GOPATH}" -type f -exec chmod 0666 {} \;
# arguments to pass on each go tool link invocation
ENV LDFLAGS="-s -w -X github.com/tarampampam/error-pages/internal/version.version=$APP_VERSION"
# -✂- this stage is used to compile the application -------------------------------------------------------------------
FROM develop AS compile
RUN set -x \
&& go version \
&& CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/ \
&& ./error-pages version \
&& ./error-pages -h
# can be passed with any prefix (like `v1.2.3@GITHASH`), e.g.: `docker build --build-arg "APP_VERSION=v1.2.3" .`
ARG APP_VERSION="undefined@docker"
RUN --mount=type=bind,source=.,target=/src set -x \
&& go generate ./... \
&& CGO_ENABLED=0 LDFLAGS="-s -w -X gh.tarampamp.am/error-pages/internal/appmeta.version=${APP_VERSION}" \
go build -trimpath -ldflags "${LDFLAGS}" -o /tmp/error-pages ./cmd/error-pages/ \
&& /tmp/error-pages --version \
&& /tmp/error-pages -h
# -✂- this stage is used to prepare the runtime fs --------------------------------------------------------------------
FROM docker.io/library/alpine:3.20 AS rootfs
WORKDIR /tmp/rootfs
# prepare rootfs for runtime
RUN set -x \
&& mkdir -p \
./etc \
./bin \
./opt/html \
RUN --mount=type=bind,source=.,target=/src set -x \
&& mkdir -p ./etc ./bin \
&& echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > ./etc/passwd \
&& echo 'appuser:x:10001:' > ./etc/group \
&& mv /src/error-pages ./bin/error-pages \
&& mv /src/templates ./opt/templates \
&& rm ./opt/templates/*.md \
&& mv /src/error-pages.yml ./opt/error-pages.yml
&& echo 'appuser:x:10001:' > ./etc/group
# take the binary from the compile stage
COPY --from=compile /tmp/error-pages ./bin/error-pages
WORKDIR /tmp/rootfs/opt
# generate static error pages (for usage inside another docker images, for example)
# generate static error pages (for use inside other Docker images, for example)
RUN set -x \
&& ./../bin/error-pages --config-file ./error-pages.yml build ./html --verbose --index \
&& mkdir ./html \
&& ./../bin/error-pages build --index --target-dir ./html \
&& ls -l ./html
# use empty filesystem
FROM scratch as runtime
# -✂- and this is the final stage (an empty filesystem is used) -------------------------------------------------------
FROM scratch AS runtime
ARG APP_VERSION="undefined@docker"
LABEL \
# Docs: <https://github.com/opencontainers/image-spec/blob/master/annotations.md>
# docs: https://github.com/opencontainers/image-spec/blob/master/annotations.md
org.opencontainers.image.title="error-pages" \
org.opencontainers.image.description="Static server error pages in the docker image" \
org.opencontainers.image.url="https://github.com/tarampampam/error-pages" \
@ -56,24 +76,22 @@ LABEL \
org.opencontainers.version="$APP_VERSION" \
org.opencontainers.image.licenses="MIT"
# Import from builder
COPY --from=builder /tmp/rootfs /
# import from builder
COPY --from=rootfs /tmp/rootfs /
# Use an unprivileged user
# use an unprivileged user
USER 10001:10001
WORKDIR /opt
ENV LISTEN_PORT="8080" \
TEMPLATE_NAME="ghost" \
DEFAULT_ERROR_PAGE="404" \
DEFAULT_HTTP_CODE="404" \
SHOW_DETAILS="false" \
DISABLE_L10N="false"
# to find out which environment variables and CLI arguments are supported by the application, run the app
# with the `--help` flag or refer to the documentation at https://github.com/tarampampam/error-pages#readme
# Docs: <https://docs.docker.com/engine/reference/builder/#healthcheck>
HEALTHCHECK --interval=7s --timeout=2s CMD ["/bin/error-pages", "healthcheck", "--log-json"]
# docs: https://docs.docker.com/reference/dockerfile/#healthcheck
HEALTHCHECK --interval=10s --start-interval=1s --start-period=5s --timeout=2s CMD [\
"/bin/error-pages", "--log-format", "json", "healthcheck" \
]
ENTRYPOINT ["/bin/error-pages"]
CMD ["serve", "--log-json"]
CMD ["--log-format", "json", "serve"]

View File

@ -1,64 +1,38 @@
#!/usr/bin/make
# Makefile readme (ru): <http://linux.yaroslavl.ru/docs/prog/gnu_make_3-79_russian_manual.html>
# Makefile readme (en): <https://www.gnu.org/software/make/manual/html_node/index.html#SEC_Contents>
SHELL = /bin/sh
LDFLAGS = "-s -w -X github.com/tarampampam/error-pages/internal/version.version=$(shell git rev-parse HEAD)"
DC_RUN_ARGS = --rm --user "$(shell id -u):$(shell id -g)"
APP_NAME = $(notdir $(CURDIR))
.PHONY : help \
image dive build fmt lint gotest int-test test shell \
up down restart \
clean
.DEFAULT_GOAL : help
.SILENT : lint gotest
# This will output the help for each task. thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help: ## Show this help
@printf "\033[33m%s:\033[0m\n" 'Available commands'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[32m%-11s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
image: ## Build docker image with app
docker build -f ./Dockerfile -t $(APP_NAME):local .
docker run --rm $(APP_NAME):local version
@printf "\n \e[30;42m %s \033[0m\n\n" 'Now you can use image like `docker run --rm -p "8080:8080/tcp" $(APP_NAME):local ...`';
.PHONY: up
up: ## Start the application in watch mode
docker compose kill web --remove-orphans 2>/dev/null || true
docker compose up --detach --wait web
$$SHELL -c "\
trap 'docker compose down --remove-orphans --timeout 30' EXIT; \
docker compose watch --no-up web \
"
dive: image ## Explore the docker image
docker run --rm -it -v "/var/run/docker.sock:/var/run/docker.sock:ro" wagoodman/dive:latest $(APP_NAME):local
.PHONY: down
down: ## Stop the application
docker compose down --remove-orphans
build: ## Build app binary file
docker-compose run $(DC_RUN_ARGS) -e "CGO_ENABLED=0" --no-deps app go build -trimpath -ldflags $(LDFLAGS) -o ./error-pages ./cmd/error-pages/
.PHONY: shell
shell: ## Start shell into development environment
docker compose run -ti $(DC_RUN_ARGS) develop bash
fmt: ## Run source code formatter tools
docker-compose run $(DC_RUN_ARGS) -e "GO111MODULE=off" --no-deps app sh -c 'go get golang.org/x/tools/cmd/goimports && $$GOPATH/bin/goimports -d -w .'
docker-compose run $(DC_RUN_ARGS) --no-deps app gofmt -s -w -d .
docker-compose run $(DC_RUN_ARGS) --no-deps app go mod tidy
.PHONY: test
test: ## Run tests
docker compose run $(DC_RUN_ARGS) develop gotestsum --format pkgname -- -race -timeout 2m ./...
lint: ## Run app linters
docker-compose run --rm --no-deps golint golangci-lint run
.PHONY: lint
lint: ## Run linters
docker compose run $(DC_RUN_ARGS) develop golangci-lint run
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
test: lint gotest int-test ## Run app tests and linters
shell: ## Start shell into container with golang
docker-compose run $(DC_RUN_ARGS) app bash
up: ## Create and start containers
docker-compose up --detach web
@printf "\n \e[30;42m %s \033[0m\n\n" 'Navigate your browser to ⇒ http://127.0.0.1:8080';
down: ## Stop all services
docker-compose down -t 5
restart: down up ## Restart all containers
clean: ## Make clean
docker-compose down -v -t 1
-docker rmi $(APP_NAME):local -f
.PHONY: gen
gen: ## Generate code
docker compose run $(DC_RUN_ARGS) develop go generate ./...

256
README.md
View File

@ -1,3 +1,241 @@
<!--GENERATED:CLI_DOCS-->
<!-- Documentation inside this block generated by github.com/urfave/cli; DO NOT EDIT -->
## CLI interface
Usage:
```bash
$ error-pages [GLOBAL FLAGS] [COMMAND] [COMMAND FLAGS] [ARGUMENTS...]
```
Global flags:
| Name | Description | Default value | Environment variables |
|--------------------|---------------------------------------|:-------------:|:---------------------:|
| `--log-level="…"` | logging level (debug/info/warn/error) | `info` | `LOG_LEVEL` |
| `--log-format="…"` | logging format (console/json) | `console` | `LOG_FORMAT` |
### `serve` command (aliases: `s`, `server`, `http`)
Start HTTP server.
Usage:
```bash
$ error-pages [GLOBAL FLAGS] serve [COMMAND FLAGS] [ARGUMENTS...]
```
The following flags are supported:
| Name | Description | Default value | Environment variables |
|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------:|:---------------------------:|
| `--listen="…"` (`-l`) | the HTTP server will listen on this IP (v4 or v6) address (set 127.0.0.1 for localhost, 0.0.0.0 to listen on all interfaces, or specify a custom IP) | `0.0.0.0` | `LISTEN_ADDR` |
| `--port="…"` (`-p`) | the TCP port number for the HTTP server to listen on (0-65535) | `8080` | `LISTEN_PORT` |
| `--add-template="…"` | to add a new template, provide the path to the file using this flag (the filename without the extension will be used as the template name) | `[]` | *none* |
| `--disable-template="…"` | disable the specified template by its name (useful to disable the built-in templates and use only custom ones) | `[]` | *none* |
| `--add-http-code="…"` (`--add-code`) | to add a new HTTP status code, provide the code and its message/description using this flag (the format should be '%code%=%message%/%description%'; the code may contain a wildcard '*' to cover multiple codes at once, for example, '4**' will cover all 4xx codes unless a more specific code is described previously) | `map[]` | *none* |
| `--json-format="…"` | override the default error page response in JSON format (Go templates are supported; the error page will use this template if the client requests JSON content type) | | `RESPONSE_JSON_FORMAT` |
| `--xml-format="…"` | override the default error page response in XML format (Go templates are supported; the error page will use this template if the client requests XML content type) | | `RESPONSE_XML_FORMAT` |
| `--plaintext-format="…"` | override the default error page response in plain text format (Go templates are supported; the error page will use this template if the client requests plain text content type or does not specify any) | | `RESPONSE_PLAINTEXT_FORMAT` |
| `--template-name="…"` (`-t`) | name of the template to use for rendering error pages (built-in templates: app-down, cats, connection, ghost, hacker-terminal, l7, lost-in-space, noise, orient, shuffle) | `app-down` | `TEMPLATE_NAME` |
| `--disable-l10n` | disable localization of error pages (if the template supports localization) | `false` | `DISABLE_L10N` |
| `--default-error-page="…"` | the code of the default (index page, when a code is not specified) error page to render | `404` | `DEFAULT_ERROR_PAGE` |
| `--send-same-http-code` | the HTTP response should have the same status code as the requested error page (by default, every response with an error page will have a status code of 200) | `false` | `SEND_SAME_HTTP_CODE` |
| `--show-details` | show request details in the error page response (if supported by the template) | `false` | `SHOW_DETAILS` |
| `--proxy-headers="…"` | HTTP headers listed here will be proxied from the original request to the error page response (comma-separated list) | `X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id` | `PROXY_HTTP_HEADERS` |
| `--rotation-mode="…"` | templates automatic rotation mode (disabled/random-on-startup/random-on-each-request/random-hourly/random-daily) | `disabled` | `TEMPLATES_ROTATION_MODE` |
### `build` command (aliases: `b`)
Build the static error pages and put them into a specified directory.
Usage:
```bash
$ error-pages [GLOBAL FLAGS] build [COMMAND FLAGS] [ARGUMENTS...]
```
The following flags are supported:
| Name | Description | Default value | Environment variables |
|---------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------:|:---------------------:|
| `--add-template="…"` | to add a new template, provide the path to the file using this flag (the filename without the extension will be used as the template name) | `[]` | *none* |
| `--disable-template="…"` | disable the specified template by its name (useful to disable the built-in templates and use only custom ones) | `[]` | *none* |
| `--add-http-code="…"` (`--add-code`) | to add a new HTTP status code, provide the code and its message/description using this flag (the format should be '%code%=%message%/%description%'; the code may contain a wildcard '*' to cover multiple codes at once, for example, '4**' will cover all 4xx codes unless a more specific code is described previously) | `map[]` | *none* |
| `--disable-l10n` | disable localization of error pages (if the template supports localization) | `false` | `DISABLE_L10N` |
| `--index` (`-i`) | generate index.html file with links to all error pages | `false` | *none* |
| `--target-dir="…"` (`--out`, `--dir`, `-o`) | directory to put the built error pages into | `.` | *none* |
### `healthcheck` command (aliases: `chk`, `health`, `check`)
Health checker for the HTTP server. The use case - docker health check.
Usage:
```bash
$ error-pages [GLOBAL FLAGS] healthcheck [COMMAND FLAGS] [ARGUMENTS...]
```
The following flags are supported:
| Name | Description | Default value | Environment variables |
|---------------------|-----------------------------------------------|:-------------:|:---------------------:|
| `--port="…"` (`-p`) | TCP port number with the HTTP server to check | `8080` | `LISTEN_PORT` |
<!--/GENERATED:CLI_DOCS-->
## 🪂 Templates (themes)
The following templates are built-in and available for use without any additional setup:
<table>
<thead>
<tr>
<th>Template</th>
<th>Preview</th>
</tr>
</thead>
<tbody>
<tr>
<td align="center">
<code>app-down</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Fapp-down.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/tarampampam/error-pages/assets/7326800/4e668a56-a4c4-47cd-ac4d-b6b45db54ab8">
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/ad4b4fd7-7c7b-4bdc-a6b6-44f9ba7f77ca">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>cats</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Fcats.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/tarampampam/error-pages/assets/7326800/5689880b-f770-406c-81dd-2d28629e6f2e">
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/056cd00e-bc9a-4120-8325-310d7b0ebd1b">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>connection</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Fconnection.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/tarampampam/error-pages/assets/7326800/3f03dc1b-c1ee-4a91-b3d7-e3b93c79020e">
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/099ecc2d-e724-4d9c-b5ed-66ddabd71139">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>ghost</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Fghost.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/tarampampam/error-pages/assets/7326800/714482ab-f8c1-4455-8ae8-b2ae78f7a2c6">
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/f253dfe7-96a0-4e96-915b-d4c544d4a237">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>hacker-terminal</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Fhacker-terminal.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/c197fc35-0844-43d0-9830-82440cee4559">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>l7</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Fl7.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/tarampampam/error-pages/assets/7326800/18e43ea3-6389-4459-be41-0fc6566a073f">
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/05f26669-94ec-40ce-8d67-a199cde54202">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>lost-in-space</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Flost-in-space.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/tarampampam/error-pages/assets/7326800/debf87c0-6f27-41a8-b141-ee3464cbd6cc">
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/c347e63d-13a7-46d4-81b9-b25266819a1d">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>noise</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Fnoise.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/4cc5c3bd-6ebb-4e96-bee8-02d4ad4e7266">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>orient</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Forient.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/tarampampam/error-pages/assets/7326800/bc2b0dad-c32c-4628-98f6-e3eab61dd1f2">
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/8fc0a7ea-694d-49ce-bb50-3ea032d52d1e">
</picture>
</td>
</tr>
<tr>
<td align="center">
<code>shuffle</code><br/><br/>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Fshuffle.json&query=%24.count&label=used%20times" alt="used times">
</td>
<td>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/tarampampam/error-pages/assets/7326800/7504b7c3-b0cb-4991-9ac2-759cd6c50fc0">
<img align="center" src="https://github.com/tarampampam/error-pages/assets/7326800/d2a73fc8-cf5f-4f42-bff8-cce33d8ae47e">
</picture>
</td>
</tr>
</tbody>
</table>
> [!NOTE]
> The "used times" counter increments when someone start the server with the specified template. Stats service does
> not collect any information about location, IP addresses, and so on. Moreover, the stats are open and available for
> everyone at [error-pages.goatcounter.com](https://error-pages.goatcounter.com/). This is simply a counter to display
> how often a particular template is used, nothing more.
<!--
<p align="center">
<a href="https://github.com/tarampampam/error-pages#readme"><img src="https://socialify.git.ci/tarampampam/error-pages/image?description=1&font=Raleway&forks=1&issues=1&logo=https%3A%2F%2Fhsto.org%2Fwebt%2Frm%2F9y%2Fww%2Frm9ywwx3gjv9agwkcmllhsuyo7k.png&owner=1&pulls=1&pattern=Solid&stargazers=1&theme=Dark" alt="banner" width="100%" /></a>
</p>
@ -5,14 +243,17 @@
<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>
</p>
<p align="center"><sup>22 feb. 2022 - ⚡ Our Docker image was downloaded <strong>one MILLION times</strong> from the docker hub! ⚡</sup></p>
<p align="center"><sup>
22 feb. 2022 - ⚡ Our Docker image was downloaded <strong>one MILLION times</strong> from the docker hub! ⚡<br/>
10 apr. 2023 - ⚡ <strong>Two million times</strong> from the docker hub and <strong>one million</strong> from the ghcr! ⚡
</sup></p>
One day you may want to replace the standard error pages of your HTTP server with something more original and pretty. That's what this repository was created for :) It contains:
@ -34,14 +275,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` |
@ -125,6 +364,7 @@ Transfer/sec: 140.23MB
| `app-down` | [![app-down][app-down-screen]][app-down-link] |
| `connection` | [![connection][connection-screen]][connection-link] |
| `matrix` | [![matrix][matrix-screen]][matrix-link] |
| `orient` | [![orient][orient-screen]][orient-link] |
> Note: `noise` template highly uses the CPU, be careful
@ -150,6 +390,8 @@ Transfer/sec: 140.23MB
[connection-link]:https://tarampampam.github.io/error-pages/connection/404.html
[matrix-screen]:https://hsto.org/webt/ng/tf/oi/ngtfoiolvmq6hf15kimcxmhprhk.gif
[matrix-link]:https://tarampampam.github.io/error-pages/matrix/404.html
[orient-screen]:https://hsto.org/webt/pz/eu/v_/pzeuv_lyeqr0xpusa4zfrtgk7sa.png
[orient-link]:https://tarampampam.github.io/error-pages/orient/404.html
## 🦾 Contributors
@ -199,3 +441,5 @@ This is open-sourced software licensed under the [MIT License][license].
[preview-demo]:https://tarampampam.github.io/error-pages/
[traefik]:https://github.com/traefik/traefik
[ingress-nginx]:https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx
-->

View File

@ -1,29 +1,35 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/fatih/color"
"github.com/tarampampam/error-pages/internal/cli"
"go.uber.org/automaxprocs/maxprocs"
"gh.tarampamp.am/error-pages/internal/cli"
)
// exitFn is a function for application exiting.
var exitFn = os.Exit //nolint:gochecknoglobals
// main CLI application entrypoint.
func main() { exitFn(run()) }
func main() {
// automatically set GOMAXPROCS to match Linux container CPU quota
_, _ = maxprocs.Set(maxprocs.Min(1), maxprocs.Logger(func(_ string, _ ...any) {}))
if err := run(); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
// 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]))
func run() error {
// create a context that is canceled when the user interrupts the program
var ctx, cancel = signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
if err := cmd.Execute(); err != nil {
_, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error())
return 1
}
return 0
return (cli.NewApp(filepath.Base(os.Args[0]))).Run(ctx, os.Args)
}

View File

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

View File

@ -1,57 +1,19 @@
# Docker-compose file is used only for local development. This is not production-ready example.
# yaml-language-server: $schema=https://cdn.jsdelivr.net/gh/compose-spec/compose-spec@master/schema/compose-spec.json
version: '3.8'
services:
develop:
build: {target: develop}
environment: {HOME: /tmp}
volumes: [.:/src:rw, tmp-data:/tmp:rw]
security_opt: [no-new-privileges:true]
web:
build: {target: runtime}
ports: ['8080:8080/tcp'] # open http://127.0.0.1:8080
command: --log-level debug serve --show-details --proxy-headers=X-Foo,Bar,Baz_blah
develop: # available since docker compose v2.22, https://docs.docker.com/compose/file-watch/
watch: [{action: rebuild, path: .}]
security_opt: [no-new-privileges:true]
volumes:
tmp-data: {}
golint-cache: {}
services:
app: &app-service
image: golang:1.18.0-buster # Image page: <https://hub.docker.com/_/golang>
working_dir: /src
environment:
HOME: /tmp
GOPATH: /tmp
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
- .:/src:rw
- tmp-data:/tmp:rw
web:
<<: *app-service
ports:
- "8080:8080/tcp" # Open <http://127.0.0.1:8080>
command:
- go
- run
- ./cmd/error-pages
- serve
- --verbose
- --port=8080
- --show-details
- --proxy-headers=X-Foo,Bar,Baz_blah
healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/healthz']
interval: 5s
timeout: 2s
golint:
image: golangci/golangci-lint:v1.44-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint>
environment:
GOLANGCI_LINT_CACHE: /tmp/golint # <https://github.com/golangci/golangci-lint/blob/v1.42.0/internal/cache/default.go#L68>
volumes:
- .:/src:ro
- golint-cache:/tmp/golint:rw
working_dir: /src
command: /bin/true
hurl:
image: orangeopensource/hurl:1.5.0
volumes:
- .:/src:ro
working_dir: /src
depends_on:
web:
condition: service_healthy

View File

@ -1,139 +0,0 @@
templates:
# - name: {string} Template name (optional, if path is defined)
# path: {string} Path to the template file
# content: {string} Template content, if path is not defined
- path: ./templates/ghost.html
name: ghost # name is optional, if path is defined
content: ${GHOST_TEMPLATE_CONTENT}
- path: ./templates/l7-light.html
- path: ./templates/l7-dark.html
- path: ./templates/shuffle.html
- path: ./templates/noise.html
- path: ./templates/hacker-terminal.html
- path: ./templates/cats.html
- path: ./templates/lost-in-space.html
- path: ./templates/app-down.html
- path: ./templates/connection.html
- path: ./templates/matrix.html
formats:
json:
content: |
{
"error": true,
"code": {{ code | json }},
"message": {{ message | json }},
"description": {{ description | json }}{{ if show_details }},
"details": {
"host": {{ host | json }},
"original_uri": {{ original_uri | json }},
"forwarded_for": {{ forwarded_for | json }},
"namespace": {{ namespace | json }},
"ingress_name": {{ ingress_name | json }},
"service_name": {{ service_name | json }},
"service_port": {{ service_port | json }},
"request_id": {{ request_id | json }},
"timestamp": {{ now.Unix }}
}{{ end }}
}
xml:
content: |
<?xml version="1.0" encoding="utf-8"?>
<error>
<code>{{ code }}</code>
<message>{{ message }}</message>
<description>{{ description }}</description>{{ if show_details }}
<details>
<host>{{ host }}</host>
<originalURI>{{ original_uri }}</originalURI>
<forwardedFor>{{ forwarded_for }}</forwardedFor>
<namespace>{{ namespace }}</namespace>
<ingressName>{{ ingress_name }}</ingressName>
<serviceName>{{ service_name }}</serviceName>
<servicePort>{{ service_port }}</servicePort>
<requestID>{{ request_id }}</requestID>
<timestamp>{{ now.Unix }}</timestamp>
</details>{{ end }}
</error>
pages:
400:
message: Bad Request
description: The server did not understand the request
401:
message: Unauthorized
description: The requested page needs a username and a password
403:
message: Forbidden
description: Access is forbidden to the requested page
404:
message: Not Found
description: The server can not find the requested page
405:
message: Method Not Allowed
description: The method specified in the request is not allowed
407:
message: Proxy Authentication Required
description: You must authenticate with a proxy server before this request can be served
408:
message: Request Timeout
description: The request took longer than the server was prepared to wait
409:
message: Conflict
description: The request could not be completed because of a conflict
410:
message: Gone
description: The requested page is no longer available
411:
message: Length Required
description: The "Content-Length" is not defined. The server will not accept the request without it
412:
message: Precondition Failed
description: The pre condition given in the request evaluated to false by the server
413:
message: Payload Too Large
description: The server will not accept the request, because the request entity is too large
416:
message: Requested Range Not Satisfiable
description: The requested byte range is not available and is out of bounds
418:
message: I'm a teapot
description: Attempt to brew coffee with a teapot is not supported
429:
message: Too Many Requests
description: Too many requests in a given amount of time
500:
message: Internal Server Error
description: The server met an unexpected condition
502:
message: Bad Gateway
description: The server received an invalid response from the upstream server
503:
message: Service Unavailable
description: The server is temporarily overloading or down
504:
message: Gateway Timeout
description: The gateway has timed out
505:
message: HTTP Version Not Supported
description: The server does not support the "http protocol" version

47
go.mod
View File

@ -1,41 +1,26 @@
module github.com/tarampampam/error-pages
module gh.tarampamp.am/error-pages
go 1.18
go 1.22
require (
github.com/a8m/envsubst v1.3.0
github.com/fasthttp/router v1.4.7
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.1
github.com/prometheus/client_model v0.2.0
github.com/spf13/cobra v1.4.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.1
github.com/valyala/fasthttp v1.34.0
go.uber.org/zap v1.21.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
github.com/stretchr/testify v1.9.0
github.com/urfave/cli-docs/v3 v3.0.0-alpha5
github.com/urfave/cli/v3 v3.0.0-alpha9
go.uber.org/automaxprocs v1.5.3
)
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/andybalholm/brotli v1.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // 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/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/klauspost/compress v1.17.9 // indirect
github.com/kr/pretty v0.3.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/savsgio/gotils v0.0.0-20220323135742-7576ce6963fd // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
google.golang.org/protobuf v1.27.1 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

565
go.sum
View File

@ -1,540 +1,45 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/a8m/envsubst v1.3.0 h1:GmXKmVssap0YtlU3E230W98RWtWCyIZzjtf1apWWyAg=
github.com/a8m/envsubst v1.3.0/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/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.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp/router v1.4.7 h1:G0kCNSx859t9U9akXANfSnYDmXngETipVPZfGGnGO8g=
github.com/fasthttp/router v1.4.7/go.mod h1:auS9NLoeFXaVcw1lHqe+LDLbb26QidGKtAQDZJ6jAMI=
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=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/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=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
github.com/kisielk/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/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=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/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.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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-20220323135742-7576ce6963fd h1:3URMJjR2af28gZjgZf5zJreZfq8EqXnRMj5fV2XdwqI=
github.com/savsgio/gotils v0.0.0-20220323135742-7576ce6963fd/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.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5 h1:H1oWnR2/GN0dNm2PVylws+GxSOD6YOwW/jI5l78YfPk=
github.com/urfave/cli-docs/v3 v3.0.0-alpha5/go.mod h1:AIqom6Q60U4tiqHp41i7+/AB2XHgi1WvQ7jOFlccmZ4=
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=
github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
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.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 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=
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=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-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-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-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/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=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

2
internal/appmeta/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package appmeta provides the application metadata, such as version.
package appmeta

View File

@ -1,5 +1,4 @@
// Package version is used as a place, where application version defined.
package version
package appmeta
import "strings"
@ -8,7 +7,7 @@ var version = "v0.0.0@undefined"
// Version returns version value (without `v` prefix).
func Version() string {
v := strings.TrimSpace(version)
var v = strings.TrimSpace(version)
if len(v) > 1 && ((v[0] == 'v' || v[0] == 'V') && (v[1] >= '0' && v[1] <= '9')) {
return v[1:]

View File

@ -1,10 +1,10 @@
package version
package appmeta
import (
"testing"
)
import "testing"
func TestVersion(t *testing.T) {
t.Parallel()
for give, want := range map[string]string{
// without changes
"vvv": "vvv",

View File

@ -1,54 +0,0 @@
// Package breaker provides OSSignals struct for OS signals handling (with context).
package breaker
import (
"context"
"os"
"os/signal"
"syscall"
)
// OSSignals allows subscribing for system signals.
type OSSignals struct {
ctx context.Context
ch chan os.Signal
}
// NewOSSignals creates new subscriber for system signals.
func NewOSSignals(ctx context.Context) OSSignals {
return OSSignals{
ctx: ctx,
ch: make(chan os.Signal, 1),
}
}
// Subscribe for some system signals (call Stop for stopping).
func (oss *OSSignals) Subscribe(onSignal func(os.Signal), signals ...os.Signal) {
if len(signals) == 0 {
signals = []os.Signal{os.Interrupt, syscall.SIGINT, syscall.SIGTERM} // default signals
}
signal.Notify(oss.ch, signals...)
go func(ch <-chan os.Signal) {
select {
case <-oss.ctx.Done():
break
case sig, opened := <-ch:
if oss.ctx.Err() != nil {
break
}
if opened && sig != nil {
onSignal(sig)
}
}
}(oss.ch)
}
// Stop system signals listening.
func (oss *OSSignals) Stop() {
signal.Stop(oss.ch)
close(oss.ch)
}

View File

@ -1,56 +0,0 @@
package breaker_test
import (
"context"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/breaker"
)
func TestNewOSSignals(t *testing.T) {
oss := breaker.NewOSSignals(context.Background())
gotSignal := make(chan os.Signal, 1)
oss.Subscribe(func(signal os.Signal) {
gotSignal <- signal
}, syscall.SIGUSR2)
defer oss.Stop()
proc, err := os.FindProcess(os.Getpid())
assert.NoError(t, err)
assert.NoError(t, proc.Signal(syscall.SIGUSR2)) // send the signal
time.Sleep(time.Millisecond * 5)
assert.Equal(t, syscall.SIGUSR2, <-gotSignal)
}
func TestNewOSSignalCtxCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
oss := breaker.NewOSSignals(ctx)
gotSignal := make(chan os.Signal, 1)
oss.Subscribe(func(signal os.Signal) {
gotSignal <- signal
}, syscall.SIGUSR2)
defer oss.Stop()
proc, err := os.FindProcess(os.Getpid())
assert.NoError(t, err)
cancel()
assert.NoError(t, proc.Signal(syscall.SIGUSR2)) // send the signal
assert.Empty(t, gotSignal)
}

View File

@ -1,56 +0,0 @@
package checkers
import (
"context"
"fmt"
"net/http"
"time"
)
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// HealthChecker is a heals checker.
type HealthChecker struct {
ctx context.Context
httpClient httpClient
}
const defaultHTTPClientTimeout = time.Second * 3
// NewHealthChecker creates heals checker.
func NewHealthChecker(ctx context.Context, client ...httpClient) *HealthChecker {
var c httpClient
if len(client) == 1 {
c = client[0]
} else {
c = &http.Client{Timeout: defaultHTTPClientTimeout} // default
}
return &HealthChecker{ctx: ctx, httpClient: c}
}
// Check application using liveness probe.
func (c *HealthChecker) Check(port uint16) error {
req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/healthz", port), nil) //nolint:lll
if err != nil {
return err
}
req.Header.Set("User-Agent", "HealthChecker/internal")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
if code := resp.StatusCode; code != http.StatusOK {
return fmt.Errorf("wrong status code [%d] from live endpoint", code)
}
return nil
}

View File

@ -1,48 +0,0 @@
package checkers_test
import (
"bytes"
"context"
"io/ioutil"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/checkers"
)
type httpClientFunc func(*http.Request) (*http.Response, error)
func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { return f(req) }
func TestHealthChecker_CheckSuccess(t *testing.T) {
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "http://127.0.0.1:123/healthz", req.URL.String())
assert.Equal(t, "HealthChecker/internal", req.Header.Get("User-Agent"))
return &http.Response{
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
StatusCode: http.StatusOK,
}, nil
}
checker := checkers.NewHealthChecker(context.Background(), httpMock)
assert.NoError(t, checker.Check(123))
}
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{})),
StatusCode: http.StatusBadGateway,
}, nil
}
checker := checkers.NewHealthChecker(context.Background(), httpMock)
err := checker.Check(123)
assert.Error(t, err)
assert.Contains(t, err.Error(), "wrong status code")
}

View File

@ -1,10 +0,0 @@
package checkers
// LiveChecker is a liveness checker.
type LiveChecker struct{}
// NewLiveChecker creates liveness checker.
func NewLiveChecker() *LiveChecker { return &LiveChecker{} }
// Check application is alive?
func (*LiveChecker) Check() error { return nil }

View File

@ -1,12 +0,0 @@
package checkers_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/checkers"
)
func TestLiveChecker_Check(t *testing.T) {
assert.NoError(t, checkers.NewLiveChecker().Check())
}

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

@ -0,0 +1,91 @@
package cli
import (
"context"
"fmt"
"runtime"
"strings"
_ "github.com/urfave/cli-docs/v3" // required for `go generate` to work
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/internal/cli/build"
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
"gh.tarampamp.am/error-pages/internal/cli/perftest"
"gh.tarampamp.am/error-pages/internal/cli/serve"
"gh.tarampamp.am/error-pages/internal/logger"
)
//go:generate go run update_readme.go
// NewApp creates a new console application.
func NewApp(appName string) *cli.Command { //nolint:funlen
var (
logLevelFlag = cli.StringFlag{
Name: "log-level",
Value: logger.InfoLevel.String(),
Usage: "logging level (" + strings.Join(logger.LevelStrings(), "/") + ")",
Sources: cli.EnvVars("LOG_LEVEL"),
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
Validator: func(s string) error {
if _, err := logger.ParseLevel(s); err != nil {
return err
}
return nil
},
}
logFormatFlag = cli.StringFlag{
Name: "log-format",
Value: logger.ConsoleFormat.String(),
Usage: "logging format (" + strings.Join(logger.FormatStrings(), "/") + ")",
Sources: cli.EnvVars("LOG_FORMAT"),
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
Validator: func(s string) error {
if _, err := logger.ParseFormat(s); err != nil {
return err
}
return nil
},
}
)
// create a "default" logger (will be swapped later with customized)
var log, _ = logger.New(logger.InfoLevel, logger.ConsoleFormat) // error will never occur
return &cli.Command{
Usage: appName,
Suggest: true,
Before: func(ctx context.Context, c *cli.Command) error {
var (
logLevel, _ = logger.ParseLevel(c.String(logLevelFlag.Name)) // error ignored because the flag validates itself
logFormat, _ = logger.ParseFormat(c.String(logFormatFlag.Name)) // --//--
)
configured, err := logger.New(logLevel, logFormat) // create a new logger instance
if err != nil {
return err
}
*log = *configured // swap the "default" logger with customized
return nil
},
Commands: []*cli.Command{
serve.NewCommand(log),
build.NewCommand(log),
healthcheck.NewCommand(log, healthcheck.NewHTTPHealthChecker()),
perftest.NewCommand(log),
},
Version: fmt.Sprintf("%s (%s)", appmeta.Version(), runtime.Version()),
Flags: []cli.Flag{ // global flags
&logLevelFlag,
&logFormatFlag,
},
}
}

18
internal/cli/app_test.go Normal file
View File

@ -0,0 +1,18 @@
package cli_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/cli"
)
func TestNewApp(t *testing.T) {
t.Parallel()
app := cli.NewApp("appName")
assert.NoError(t, app.Run(context.Background(), []string{""}))
}

View File

@ -1,148 +1,232 @@
package build
import (
"context"
_ "embed"
"errors"
"fmt"
"html/template"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/tpl"
"go.uber.org/zap"
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/logger"
appTemplate "gh.tarampamp.am/error-pages/internal/template"
)
// NewCommand creates `build` command.
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command {
var (
generateIndex bool
disableL10n bool
cfg *config.Config
)
//go:embed index.html
var indexHtml string
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")
}
type command struct {
c *cli.Command
if cfg, err = config.FromYamlFile(*configFile); err != nil {
return err
}
return
},
RunE: func(_ *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("wrong arguments count")
}
return run(log, cfg, args[0], generateIndex, disableL10n)
},
opt struct {
createIndex bool
targetDirAbsPath string
}
cmd.Flags().BoolVarP(
&generateIndex,
"index", "i",
false,
"generate index page",
)
cmd.Flags().BoolVarP(
&disableL10n,
"disable-l10n", "",
false,
"disable error pages localization",
)
return cmd
}
const (
outHTMLFileExt = ".html"
outIndexFileName = "index"
outFilePerm = os.FileMode(0664)
outDirPerm = os.FileMode(0775)
)
// NewCommand creates `build` command.
func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
var (
cmd command
cfg = config.New()
func run(log *zap.Logger, cfg *config.Config, outDirectoryPath string, generateIndex, disableL10n bool) error { //nolint:funlen,lll
if len(cfg.Templates) == 0 {
return errors.New("no loaded templates")
}
addTplFlag = shared.AddTemplatesFlag
disableTplFlag = shared.DisableTemplateNamesFlag
addCodeFlag = shared.AddHTTPCodesFlag
disableL10nFlag = shared.DisableL10nFlag
createIndexFlag = cli.BoolFlag{
Name: "index",
Aliases: []string{"i"},
Usage: "generate index.html file with links to all error pages",
}
targetDirFlag = cli.StringFlag{
Name: "target-dir",
Aliases: []string{"out", "dir", "o"},
Usage: "directory to put the built error pages into",
Value: ".", // current directory by default
Config: cli.StringConfig{TrimSpace: true},
OnlyOnce: true,
Validator: func(dir string) error {
if dir == "" {
return errors.New("missing target directory")
}
log.Info("output directory preparing", zap.String("path", outDirectoryPath))
if stat, err := os.Stat(dir); err != nil {
return fmt.Errorf("cannot access the target directory '%s': %w", dir, err)
} else if !stat.IsDir() {
return fmt.Errorf("'%s' is not a directory", dir)
}
if err := createDirectory(outDirectoryPath, outDirPerm); err != nil {
return errors.Wrap(err, "cannot prepare output directory")
}
return nil
},
}
)
history, renderer := newBuildingHistory(), tpl.NewTemplateRenderer()
defer func() { _ = renderer.Close() }()
disableL10nFlag.Value = cfg.L10n.Disable // set the default value depending on the configuration
for _, template := range cfg.Templates {
log.Debug("template processing", zap.String("name", template.Name()))
cmd.c = &cli.Command{
Name: "build",
Aliases: []string{"b"},
Usage: "Build the static error pages and put them into a specified directory",
Action: func(ctx context.Context, c *cli.Command) error {
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
cmd.opt.createIndex = c.Bool(createIndexFlag.Name)
cmd.opt.targetDirAbsPath, _ = filepath.Abs(c.String(targetDirFlag.Name)) // an error checked by [os.Stat] validator
for _, page := range cfg.Pages {
if err := createDirectory(path.Join(outDirectoryPath, template.Name()), outDirPerm); err != nil {
return err
// add templates from files to the configuration
if add := c.StringSlice(addTplFlag.Name); len(add) > 0 {
for _, templatePath := range add {
if addedName, err := cfg.Templates.AddFromFile(templatePath); err != nil {
return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
} else {
log.Info("Template added",
logger.String("name", addedName),
logger.String("path", templatePath),
)
}
}
}
var (
fileName = page.Code() + outHTMLFileExt
filePath = path.Join(outDirectoryPath, template.Name(), fileName)
// disable templates specified by the user
if disable := c.StringSlice(disableTplFlag.Name); len(disable) > 0 {
for _, templateName := range disable {
if ok := cfg.Templates.Remove(templateName); ok {
log.Info("Template disabled", logger.String("name", templateName))
}
}
}
// add custom HTTP codes to the configuration
if add := c.StringMap(addCodeFlag.Name); len(add) > 0 {
for code, desc := range shared.ParseHTTPCodes(add) {
cfg.Codes[code] = desc
log.Info("HTTP code added",
logger.String("code", code),
logger.String("message", desc.Message),
logger.String("description", desc.Description),
)
}
}
if len(cfg.Templates) == 0 {
return errors.New("no templates specified")
}
log.Info("Building error pages",
logger.String("targetDir", cmd.opt.targetDirAbsPath),
logger.Strings("templates", cfg.Templates.Names()...),
logger.Bool("index", cmd.opt.createIndex),
logger.Bool("l10n", !cfg.L10n.Disable),
)
content, renderingErr := renderer.Render(template.Content(), tpl.Properties{
Code: page.Code(),
Message: page.Message(),
Description: page.Description(),
return cmd.Run(ctx, log, &cfg)
},
Flags: []cli.Flag{
&addTplFlag,
&disableTplFlag,
&addCodeFlag,
&disableL10nFlag,
&createIndexFlag,
&targetDirFlag,
},
}
return cmd.c
}
func (cmd *command) Run( //nolint:funlen
ctx context.Context,
log *logger.Logger,
cfg *config.Config,
) error {
type historyItem struct{ Code, Message, RelativePath string }
var history = make(map[string][]historyItem, len(cfg.Codes)*len(cfg.Templates)) // map[template_name]codes
for templateName, templateContent := range cfg.Templates {
log.Debug("Processing template", logger.String("name", templateName))
for code, codeDescription := range cfg.Codes {
if err := createDirectory(filepath.Join(cmd.opt.targetDirAbsPath, templateName)); err != nil {
return fmt.Errorf("cannot create directory for template '%s': %w", templateName, err)
}
var codeAsUint, codeParsingErr = strconv.ParseUint(code, 10, 32)
if codeParsingErr != nil {
log.Warn("Cannot parse code", logger.String("code", code))
continue
}
var outFilePath = path.Join(cmd.opt.targetDirAbsPath, templateName, code+".html")
if content, renderErr := appTemplate.Render(templateContent, appTemplate.Props{
Code: uint16(codeAsUint),
Message: codeDescription.Message,
Description: codeDescription.Description,
L10nDisabled: cfg.L10n.Disable,
ShowRequestDetails: false,
L10nDisabled: disableL10n,
}); renderErr == nil {
if err := os.WriteFile(outFilePath, []byte(content), os.FileMode(0664)); err != nil { //nolint:mnd
return err
}
} else {
return fmt.Errorf("cannot render template '%s': %w", templateName, renderErr)
}
log.Debug("Page built", logger.String("template", templateName), logger.String("code", code))
history[templateName] = append(history[templateName], historyItem{
Code: code,
Message: codeDescription.Message,
RelativePath: "." + strings.TrimPrefix(outFilePath, cmd.opt.targetDirAbsPath), // to make it relative
})
if renderingErr != nil {
return renderingErr
}
if err := os.WriteFile(filePath, content, outFilePerm); err != nil {
return err
}
log.Debug("page rendered", zap.String("path", filePath))
if generateIndex {
history.Append(
template.Name(),
page.Code(),
page.Message(),
path.Join(template.Name(), fileName),
)
}
}
}
if generateIndex {
var filepath = path.Join(outDirectoryPath, outIndexFileName+outHTMLFileExt)
if cmd.opt.createIndex {
log.Debug("Creating the index file")
log.Info("index file generation", zap.String("path", filepath))
for name := range history {
slices.SortFunc(history[name], func(a, b historyItem) int { return strings.Compare(a.Code, b.Code) })
}
if err := history.WriteIndexFile(filepath, outFilePerm); err != nil {
indexTpl, tplErr := template.New("index").Parse(indexHtml)
if tplErr != nil {
return tplErr
}
var buf strings.Builder
if err := indexTpl.Execute(&buf, history); err != nil {
return err
}
}
log.Info("job is done")
return os.WriteFile(
filepath.Join(cmd.opt.targetDirAbsPath, "index.html"),
[]byte(buf.String()),
os.FileMode(0664), //nolint:mnd
)
}
return nil
}
func createDirectory(path string, perm os.FileMode) error {
stat, err := os.Stat(path)
func createDirectory(path string) error {
var stat, err = os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(path, perm)
return os.MkdirAll(path, os.FileMode(0775)) //nolint:mnd
}
return err

View File

@ -1,7 +0,0 @@
package build_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,59 +0,0 @@
package build
import (
"bytes"
_ "embed"
"os"
"sort"
"text/template"
)
type (
buildingHistory struct {
items map[string][]historyItem
}
historyItem struct {
Code, Message, Path string
}
)
func newBuildingHistory() buildingHistory {
return buildingHistory{items: make(map[string][]historyItem)}
}
func (bh *buildingHistory) Append(templateName, pageCode, message, path string) {
if _, ok := bh.items[templateName]; !ok {
bh.items[templateName] = make([]historyItem, 0)
}
bh.items[templateName] = append(bh.items[templateName], historyItem{
Code: pageCode,
Message: message,
Path: path,
})
sort.Slice(bh.items[templateName], func(i, j int) bool { // keep history items sorted
return bh.items[templateName][i].Code < bh.items[templateName][j].Code
})
}
//go:embed index.tpl.html
var indexPageTemplate string
func (bh *buildingHistory) WriteIndexFile(path string, perm os.FileMode) error {
t, err := template.New("index").Parse(indexPageTemplate)
if err != nil {
return err
}
var buf bytes.Buffer
if err = t.Execute(&buf, bh.items); err != nil {
return err
}
defer buf.Reset() // optimization (is needed here?)
return os.WriteFile(path, buf.Bytes(), perm)
}

View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="robots" content="follow,index">
<title>Error pages list</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
:root {
--color-primary: #fff;
--color-inverted: #202020;
--color-link: #395364;
}
@media (prefers-color-scheme: dark) {
:root {
--color-primary: #1a1a1a;
--color-inverted: #fff;
--color-link: #5cb0d3;
}
}
html, body {
margin: 0;
padding: 0;
min-height: 100%;
height: 100%;
width: 100%;
background-color: var(--color-primary);
color: var(--color-inverted);
font-family: sans-serif;
font-size: 16px;
}
@media screen and (min-width: 2000px) {
html, body {
font-size: 20px;
}
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
a {
color: var(--color-link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
main {
width: 1200px;
height: 100%;
text-align: left;
}
header {
padding: 3em 0;
}
header h1 {
font-size: 3em;
text-align: center;
margin: 0;
}
article h2 {
font-size: 1.7em;
}
article h2 code {
text-decoration: underline;
}
article ul {
list-style: none;
margin: 1em 0;
padding: 0 0 0 1em;
}
article ul li {
margin: 0;
padding: 0;
}
footer {
padding: 3em 0;
text-align: center;
font-size: 0.8em;
}
</style>
</head>
<body>
<main>
<header>
<h1>Error pages index</h1>
</header>
<article>
<!-- {{- range $templateName, $details := . -}} -->
<h2>Template name: <Code>{{ $templateName }}</Code></h2>
<ul class="mb-5">
<!-- {{ range $details -}}-->
<li><a href="{{ .RelativePath }}"><strong>{{ .Code }}</strong>: {{ .Message }}</a></li>
<!-- {{ end -}} -->
</ul>
<!-- {{ end }} -->
</article>
<footer>
For online documentation and support please refer to the
<a href="https://gh.tarampamp.am/error-pages">project repository</a>.
</footer>
</main>
</body>
</html>

View File

@ -1,41 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Error pages list</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.3/css/bootstrap.min.css"
integrity="sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
@media (prefers-color-scheme:dark){
:root {--bs-light:#212529;--bs-light-rgb:33,37,41;--bs-body-color:#eee}a{color:#91b4e8}a:hover{color:#a2bfec}
}
</style>
</head>
<body class="bg-light">
<div class="container">
<main>
<div class="py-5 text-center">
<img class="d-block mx-auto mb-4" src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png"
alt="" width="94">
<h2>Error pages index</h2>
</div>
{{- range $template, $item := . -}}
<h2 class="mb-3">Template name: <Code>{{ $template }}</Code></h2>
<ul class="mb-5">
{{ range $item -}}
<li><a href="{{ .Path }}"><strong>{{ .Code }}</strong>: {{ .Message }}</a></li>
{{ end -}}
</ul>
{{ end }}
</main>
</div>
<footer class="footer">
<div class="container text-center text-muted mt-3 mb-3">
For online documentation and support please refer to the
<a href="https://github.com/tarampampam/error-pages">project repository</a>.
</div>
</footer>
</body>
</html>

View File

@ -0,0 +1,89 @@
package healthcheck
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"strings"
"time"
"gh.tarampamp.am/error-pages/internal/appmeta"
)
type (
httpClient interface {
Do(*http.Request) (*http.Response, error)
}
// HealthCheckerOption allows you to change some settings of the checker.
HealthCheckerOption func(*HTTPHealthChecker)
)
// WithHttpClient allows to set http client.
func WithHttpClient(c httpClient) HealthCheckerOption {
return func(hc *HTTPHealthChecker) { hc.httpClient = c }
}
// WithLiveEndpoint set the endpoint to check.
func WithLiveEndpoint(endpoint string) HealthCheckerOption {
if len(endpoint) > 0 && endpoint[0] != '/' {
endpoint = "/" + endpoint
}
return func(hc *HTTPHealthChecker) { hc.liveEndpoint = endpoint }
}
// HTTPHealthChecker is HTTP probe checker.
type HTTPHealthChecker struct {
httpClient httpClient
liveEndpoint string
}
var _ checker = (*HTTPHealthChecker)(nil) // ensure that HTTPHealthChecker implements checker interface
func NewHTTPHealthChecker(opts ...HealthCheckerOption) *HTTPHealthChecker {
const (
httpClientTimeout = 3 * time.Second
liveRoute = "/healthz"
)
var c = HTTPHealthChecker{
httpClient: &http.Client{
Timeout: httpClientTimeout,
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, //nolint:gosec
},
liveEndpoint: liveRoute,
}
for _, opt := range opts {
opt(&c)
}
return &c
}
// Check performs HTTP get request.
func (c *HTTPHealthChecker) Check(ctx context.Context, baseURL string) error {
var endpoint = strings.TrimRight(strings.TrimSpace(baseURL), "/") + c.liveEndpoint
var req, err = http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
if err != nil {
return err
}
req.Header.Set("User-Agent", fmt.Sprintf("ErrorPages/%s (HealthCheck)", appmeta.Version()))
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
if code := resp.StatusCode; code != http.StatusOK && code != http.StatusNoContent {
return fmt.Errorf("wrong status code [%d] from the live endpoint (%s)", code, endpoint)
}
return nil
}

View File

@ -0,0 +1,130 @@
package healthcheck_test
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gh.tarampamp.am/error-pages/internal/appmeta"
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
)
type httpClientFunc func(*http.Request) (*http.Response, error)
func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { return f(req) }
func TestHealthChecker_CheckSuccess(t *testing.T) {
t.Parallel()
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "foobar:123/healthz", req.URL.String())
assert.Equal(t, fmt.Sprintf("ErrorPages/%s (HealthCheck)", appmeta.Version()), req.Header.Get("User-Agent"))
return &http.Response{
Body: io.NopCloser(bytes.NewReader([]byte("ok"))),
StatusCode: http.StatusOK,
}, nil
}
assert.NoError(t, healthcheck.NewHTTPHealthChecker(
healthcheck.WithHttpClient(httpMock),
).Check(context.Background(), "foobar:123"))
}
func TestHealthChecker_CheckFail(t *testing.T) {
t.Parallel()
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, "foobar:123/foo", req.URL.String())
return &http.Response{
Body: http.NoBody,
StatusCode: http.StatusBadGateway,
}, nil
}
var err = healthcheck.NewHTTPHealthChecker(
healthcheck.WithHttpClient(httpMock),
healthcheck.WithLiveEndpoint("foo"),
).Check(context.Background(), "foobar:123")
assert.Error(t, err)
assert.Contains(t, err.Error(), "wrong status code [502]")
}
func TestHealthChecker_ClientDoError(t *testing.T) {
t.Parallel()
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
return nil, assert.AnError
}
var err = healthcheck.NewHTTPHealthChecker(
healthcheck.WithHttpClient(httpMock),
healthcheck.WithLiveEndpoint("foo"),
).Check(context.Background(), "foobar:123")
assert.ErrorIs(t, err, assert.AnError)
}
func TestHTTPHealthChecker_CheckNormalize(t *testing.T) {
t.Parallel()
for name, _tc := range map[string]struct {
giveBaseURL string
giveLive string
wantURL string
}{
"no-live": {
giveBaseURL: "foobar:123",
wantURL: "foobar:123",
},
"live with slash": {
giveBaseURL: "foobar:123",
giveLive: "/foo",
wantURL: "foobar:123/foo",
},
"live without slash": {
giveBaseURL: "foobar:123",
giveLive: "foo",
wantURL: "foobar:123/foo",
},
"base with slash": {
giveBaseURL: "foobar:123/",
giveLive: "foo",
wantURL: "foobar:123/foo",
},
"all of slashes": {
giveBaseURL: "foobar:123/",
giveLive: "/foo",
wantURL: "foobar:123/foo",
},
} {
tc := _tc
t.Run(name, func(t *testing.T) {
t.Parallel()
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
assert.Equal(t, tc.wantURL, req.URL.String())
return &http.Response{
Body: http.NoBody,
StatusCode: http.StatusOK,
}, nil
}
require.NoError(t, healthcheck.NewHTTPHealthChecker(
healthcheck.WithHttpClient(httpMock),
healthcheck.WithLiveEndpoint(tc.giveLive),
).Check(context.Background(), tc.giveBaseURL))
})
}
}

View File

@ -1,57 +1,34 @@
// Package healthcheck contains CLI `healthcheck` command implementation.
package healthcheck
import (
"context"
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/tarampampam/error-pages/internal/env"
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/logger"
)
type checker interface {
Check(port uint16) error
Check(ctx context.Context, baseURL string) error
}
const portFlagName = "port"
// NewCommand creates `healthcheck` command.
func NewCommand(checker checker) *cobra.Command {
var port uint16
func NewCommand(_ *logger.Logger, checker checker) *cli.Command {
var portFlag = shared.ListenPortFlag
cmd := &cobra.Command{
Use: "healthcheck",
portFlag.Usage = "TCP port number with the HTTP server to check"
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)
}
}
}
})
return lastErr
Usage: "Health checker for the HTTP server. The use case - docker health check",
Action: func(ctx context.Context, c *cli.Command) error {
return checker.Check(ctx, fmt.Sprintf("http://127.0.0.1:%d", c.Uint(portFlag.Name)))
},
RunE: func(*cobra.Command, []string) error {
return checker.Check(port)
Flags: []cli.Flag{
&portFlag,
},
}
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

@ -1,94 +1,55 @@
package healthcheck_test
import (
"errors"
"os"
"context"
"testing"
"github.com/kami-zh/go-capturer"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/cli/healthcheck"
"github.com/stretchr/testify/require"
"gh.tarampamp.am/error-pages/internal/cli/healthcheck"
"gh.tarampamp.am/error-pages/internal/logger"
)
type fakeChecker struct{ err error }
func TestNewCommand(t *testing.T) {
t.Parallel()
func (c *fakeChecker) Check(port uint16) error { return c.err }
var cmd = healthcheck.NewCommand(logger.NewNop(), nil)
func TestProperties(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
assert.Equal(t, "healthcheck", cmd.Use)
assert.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases)
assert.NotNil(t, cmd.RunE)
assert.Equal(t, "healthcheck", cmd.Name)
assert.Equal(t, []string{"chk", "health", "check"}, cmd.Aliases)
}
func TestCommandRun(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
cmd.SetArgs([]string{})
type fakeHealthChecker struct {
t *testing.T
wantAddress string
giveErr error
}
output := capturer.CaptureOutput(func() {
assert.NoError(t, cmd.Execute())
func (m *fakeHealthChecker) Check(_ context.Context, addr string) error {
assert.Equal(m.t, m.wantAddress, addr)
return m.giveErr
}
func TestCommand_RunSuccess(t *testing.T) {
var cmd = healthcheck.NewCommand(logger.NewNop(), &fakeHealthChecker{
t: t,
wantAddress: "http://127.0.0.1:1234",
})
assert.Empty(t, output)
require.NoError(t, cmd.Run(context.Background(), []string{"", "--port", "1234"}))
}
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())
func TestCommand_RunFail(t *testing.T) {
cmd := healthcheck.NewCommand(logger.NewNop(), &fakeHealthChecker{
t: t,
wantAddress: "http://127.0.0.1:4321",
giveErr: assert.AnError,
})
assert.Contains(t, output, "foo err")
}
func TestPortFlagWrongArgument(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
cmd.SetArgs([]string{"-p", "65536"}) // 65535 is max
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, "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.ErrorIs(t,
cmd.Run(context.Background(), []string{"", "--port", "4321"}),
assert.AnError,
)
}

View File

@ -0,0 +1,203 @@
package perftest
import (
"context"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/logger"
)
// NewCommand creates `perftest` command.
func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit
var (
portFlag = shared.ListenPortFlag
durationFlag = cli.DurationFlag{
Name: "duration",
Aliases: []string{"d"},
Usage: "duration of the test",
Value: 10 * time.Second, //nolint:mnd
Validator: func(d time.Duration) error {
if d <= time.Second {
return errors.New("duration can't be less than 1 second")
}
return nil
},
}
threadsFlag = cli.UintFlag{
Name: "threads",
Aliases: []string{"t"},
Usage: "number of threads",
Value: max(2, uint64(runtime.NumCPU()/2)), //nolint:mnd
Validator: func(u uint64) error {
if u == 0 {
return errors.New("threads number can't be zero")
}
return nil
},
}
)
return &cli.Command{
Name: "perftest",
Aliases: []string{"perf", "test"},
Hidden: true,
Usage: "Simple performance (load) test for the HTTP server",
Action: func(ctx context.Context, c *cli.Command) error { // TODO: use fasthttp.Client
var (
perfCtx, cancel = context.WithTimeout(ctx, c.Duration(durationFlag.Name))
startedAt = time.Now()
wg sync.WaitGroup
success atomic.Uint64
failed atomic.Uint64
)
defer func() {
cancel()
log.Info("Summary",
logger.Uint64("success", success.Load()),
logger.Uint64("failed", failed.Load()),
logger.Duration("duration", time.Since(startedAt)),
logger.Float64("RPS", float64(success.Load()+failed.Load())/time.Since(startedAt).Seconds()),
logger.Float64("errors rate", float64(failed.Load())/float64(success.Load()+failed.Load())*100), //nolint:mnd
)
}()
log.Info("Running test",
logger.Uint64("threads", c.Uint(threadsFlag.Name)),
logger.Duration("duration", c.Duration(durationFlag.Name)),
)
var httpClient = &http.Client{
Transport: &http.Transport{MaxConnsPerHost: max(2, int(c.Uint(threadsFlag.Name))-1)}, //nolint:mnd
Timeout: c.Duration(durationFlag.Name) + time.Second,
}
for i := uint64(0); i < c.Uint(threadsFlag.Name); i++ {
wg.Add(1)
go func(log *logger.Logger) {
defer wg.Done()
if perfCtx.Err() != nil {
return
}
var req, rErr = makeRequest(perfCtx, uint16(c.Uint(portFlag.Name)))
if rErr != nil {
log.Error("Failed to create a new request", logger.Error(rErr))
return
}
for {
var sentAt = time.Now()
var resp, respErr = httpClient.Do(req)
if resp != nil {
if _, err := io.Copy(io.Discard, resp.Body); err != nil && !errIsDone(err) {
log.Error("Failed to read response body", logger.Error(err))
}
if err := resp.Body.Close(); err != nil && !errIsDone(err) {
log.Error("Failed to close response body", logger.Error(err))
}
}
if respErr != nil {
if errIsDone(respErr) {
return
}
log.Error("Request failed", logger.Error(respErr))
failed.Add(1)
continue
}
log.Debug("Response received",
logger.String("status", resp.Status),
logger.Duration("duration", time.Since(sentAt)),
logger.Int64("size", resp.ContentLength),
logger.Uint64("success", success.Load()),
logger.Uint64("failed", failed.Load()),
)
success.Add(1)
}
}(log.Named(fmt.Sprintf("thread-%d", i)))
}
wg.Wait()
return nil
},
Flags: []cli.Flag{
&portFlag,
&durationFlag,
&threadsFlag,
},
}
}
// randomIntBetween returns a random integer between min and max.
func randomIntBetween(min, max int) int { return min + rand.Intn(max-min) } //nolint:gosec
// makeRequest creates a new HTTP request for the performance test.
func makeRequest(ctx context.Context, port uint16) (*http.Request, error) {
var req, rErr = http.NewRequestWithContext(ctx,
http.MethodGet,
fmt.Sprintf(
"http://127.0.0.1:%d/%d.html?rnd=%d", // for load testing purposes only
port,
randomIntBetween(400, 418), //nolint:mnd
randomIntBetween(1, 999_999_999), //nolint:mnd
),
http.NoBody,
)
if rErr != nil {
return nil, rErr
}
req.Header.Set("Connection", "keep-alive")
req.Header.Set("User-Agent", "perftest")
req.Header.Set("X-Namespace", fmt.Sprintf("namespace-%d", randomIntBetween(1, 999_999_999))) //nolint:mnd
req.Header.Set("X-Request-ID", fmt.Sprintf("req-id-%d", randomIntBetween(1, 999_999_999))) //nolint:mnd
var contentType string
switch randomIntBetween(1, 4) { //nolint:mnd
case 1:
contentType = "application/json"
case 2: //nolint:mnd
contentType = "application/xml"
case 3: //nolint:mnd
contentType = "text/html"
default:
contentType = "text/plain"
}
req.Header.Set("Content-Type", contentType)
return req, nil
}
// errIsDone checks if the error is a context.DeadlineExceeded or context.Canceled.
func errIsDone(err error) bool {
return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)
}

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,137 +3,350 @@ package serve
import (
"context"
"errors"
"os"
"fmt"
"net/http"
"net/url"
"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"
"go.uber.org/zap"
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/config"
appHttp "gh.tarampamp.am/error-pages/internal/http"
"gh.tarampamp.am/error-pages/internal/logger"
)
// NewCommand creates `serve` command.
func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command {
var (
f flags
cfg *config.Config
)
type command struct {
c *cli.Command
cmd := &cobra.Command{
Use: "serve",
Aliases: []string{"s", "server"},
Short: "Start HTTP server",
PreRunE: func(cmd *cobra.Command, _ []string) (err error) {
if configFile == nil {
return errors.New("path to the config file is required for this command")
}
if err = f.OverrideUsingEnv(cmd.Flags()); err != nil {
return err
}
if cfg, err = config.FromYamlFile(*configFile); err != nil {
return err
}
return f.Validate()
},
RunE: func(*cobra.Command, []string) error { return run(ctx, log, cfg, f) },
}
f.Init(cmd.Flags())
return cmd
}
// run current command.
func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags) error { //nolint:funlen
var (
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
oss = breaker.NewOSSignals(ctx) // OS signals listener
)
// subscribe for system signals
oss.Subscribe(func(sig os.Signal) {
log.Warn("Stopping by OS signal..", zap.String("signal", sig.String()))
cancel()
})
defer func() {
cancel() // call the cancellation function after all
oss.Stop() // stop system signals listening
}()
var (
templateNames = cfg.TemplateNames()
picker interface{ Pick() string }
opt = f.ToOptions()
)
switch opt.Template.Name {
case useRandomTemplate:
log.Info("A random template will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomOnce)
case useRandomTemplateOnEachRequest:
log.Info("A random template on EACH request will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime)
case useRandomTemplateDaily:
log.Info("A random template will be used and changed once a day")
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour*24) //nolint:gomnd
case useRandomTemplateHourly:
log.Info("A random template will be used and changed hourly")
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour)
case "":
log.Info("The first template (ordered by name) will be used")
picker = pick.NewStringsSlice(templateNames, pick.First)
default:
if t, found := cfg.Template(opt.Template.Name); found {
log.Info("We will use the requested template", zap.String("name", t.Name()))
picker = pick.NewStringsSlice([]string{t.Name()}, pick.First)
} else {
return errors.New("requested nonexistent template: " + opt.Template.Name)
opt struct {
http struct { // our HTTP server
addr string
port uint16
// readBufferSize uint
}
}
}
// create HTTP server
server := appHttp.NewServer(log)
// NewCommand creates `serve` command.
func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
var (
cmd command
cfg = config.New()
env, trim = cli.EnvVars, cli.StringConfig{TrimSpace: true}
)
// register server routes, middlewares, etc.
if err := server.Register(cfg, picker, opt); err != nil {
var (
addrFlag = shared.ListenAddrFlag
portFlag = shared.ListenPortFlag
addTplFlag = shared.AddTemplatesFlag
disableTplFlag = shared.DisableTemplateNamesFlag
addCodeFlag = shared.AddHTTPCodesFlag
disableL10nFlag = shared.DisableL10nFlag
jsonFormatFlag = cli.StringFlag{
Name: "json-format",
Usage: "override the default error page response in JSON format (Go templates are supported; the error " +
"page will use this template if the client requests JSON content type)",
Sources: env("RESPONSE_JSON_FORMAT"),
OnlyOnce: true,
Config: trim,
}
xmlFormatFlag = cli.StringFlag{
Name: "xml-format",
Usage: "override the default error page response in XML format (Go templates are supported; the error " +
"page will use this template if the client requests XML content type)",
Sources: env("RESPONSE_XML_FORMAT"),
OnlyOnce: true,
Config: trim,
}
plainTextFormatFlag = cli.StringFlag{
Name: "plaintext-format",
Usage: "override the default error page response in plain text format (Go templates are supported; the " +
"error page will use this template if the client requests plain text content type or does not specify any)",
Sources: env("RESPONSE_PLAINTEXT_FORMAT"),
OnlyOnce: true,
Config: trim,
}
templateNameFlag = cli.StringFlag{
Name: "template-name",
Aliases: []string{"t"},
Value: cfg.TemplateName,
Usage: "name of the template to use for rendering error pages (built-in templates: " +
strings.Join(cfg.Templates.Names(), ", ") + ")",
Sources: env("TEMPLATE_NAME"),
OnlyOnce: true,
Config: trim,
}
defaultCodeToRenderFlag = cli.UintFlag{
Name: "default-error-page",
Usage: "the code of the default (index page, when a code is not specified) error page to render",
Value: uint64(cfg.DefaultCodeToRender),
Sources: env("DEFAULT_ERROR_PAGE"),
Validator: func(code uint64) error {
if code > 999 { //nolint:mnd
return fmt.Errorf("wrong HTTP code [%d] for the default error page", code)
}
return nil
},
OnlyOnce: true,
}
sendSameHTTPCodeFlag = cli.BoolFlag{
Name: "send-same-http-code",
Usage: "the HTTP response should have the same status code as the requested error page (by default, " +
"every response with an error page will have a status code of 200)",
Value: cfg.RespondWithSameHTTPCode,
Sources: env("SEND_SAME_HTTP_CODE"),
OnlyOnce: true,
}
showDetailsFlag = cli.BoolFlag{
Name: "show-details",
Usage: "show request details in the error page response (if supported by the template)",
Value: cfg.ShowDetails,
Sources: env("SHOW_DETAILS"),
OnlyOnce: true,
}
proxyHeadersListFlag = cli.StringFlag{
Name: "proxy-headers",
Usage: "HTTP headers listed here will be proxied from the original request to the error page response " +
"(comma-separated list)",
Value: strings.Join(cfg.ProxyHeaders, ","),
Sources: env("PROXY_HTTP_HEADERS"),
Validator: func(s string) error {
for _, raw := range strings.Split(s, ",") {
if clean := strings.TrimSpace(raw); strings.ContainsRune(clean, ' ') {
return fmt.Errorf("whitespaces in the HTTP headers are not allowed: %s", clean)
}
}
return nil
},
OnlyOnce: true,
Config: trim,
}
rotationModeFlag = cli.StringFlag{
Name: "rotation-mode",
Value: config.RotationModeDisabled.String(),
Usage: "templates automatic rotation mode (" + strings.Join(config.RotationModeStrings(), "/") + ")",
Sources: env("TEMPLATES_ROTATION_MODE"),
OnlyOnce: true,
Config: trim,
Validator: func(s string) error {
if _, err := config.ParseRotationMode(s); err != nil {
return err
}
return nil
},
}
)
// override some flag usage messages
addrFlag.Usage = "the HTTP server will listen on this IP (v4 or v6) address (set 127.0.0.1 for localhost, " +
"0.0.0.0 to listen on all interfaces, or specify a custom IP)"
portFlag.Usage = "the TCP port number for the HTTP server to listen on (0-65535)"
disableL10nFlag.Value = cfg.L10n.Disable // set the default value depending on the configuration
cmd.c = &cli.Command{
Name: "serve",
Aliases: []string{"s", "server", "http"},
Usage: "Start HTTP server",
Suggest: true,
Action: func(ctx context.Context, c *cli.Command) error {
cmd.opt.http.addr = c.String(addrFlag.Name)
cmd.opt.http.port = uint16(c.Uint(portFlag.Name))
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
cfg.DefaultCodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name)
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
{ // override default JSON, XML, and PlainText formats
if c.IsSet(jsonFormatFlag.Name) {
cfg.Formats.JSON = strings.TrimSpace(c.String(jsonFormatFlag.Name))
}
if c.IsSet(xmlFormatFlag.Name) {
cfg.Formats.XML = strings.TrimSpace(c.String(xmlFormatFlag.Name))
}
if c.IsSet(plainTextFormatFlag.Name) {
cfg.Formats.PlainText = strings.TrimSpace(c.String(plainTextFormatFlag.Name))
}
}
// add templates from files to the configuration
if add := c.StringSlice(addTplFlag.Name); len(add) > 0 {
for _, templatePath := range add {
if addedName, err := cfg.Templates.AddFromFile(templatePath); err != nil {
return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
} else {
log.Info("Template added",
logger.String("name", addedName),
logger.String("path", templatePath),
)
}
}
}
// set the list of HTTP headers we need to proxy from the incoming request to the error page response
if c.IsSet(proxyHeadersListFlag.Name) {
var m = make(map[string]struct{}) // map is used to avoid duplicates
for _, header := range strings.Split(c.String(proxyHeadersListFlag.Name), ",") {
m[http.CanonicalHeaderKey(strings.TrimSpace(header))] = struct{}{}
}
clear(cfg.ProxyHeaders) // clear the list before adding new headers
for header := range m {
cfg.ProxyHeaders = append(cfg.ProxyHeaders, header)
}
}
// add custom HTTP codes to the configuration
if add := c.StringMap(addCodeFlag.Name); len(add) > 0 {
for code, desc := range shared.ParseHTTPCodes(add) {
cfg.Codes[code] = desc
log.Info("HTTP code added",
logger.String("code", code),
logger.String("message", desc.Message),
logger.String("description", desc.Description),
)
}
}
// disable templates specified by the user
if disable := c.StringSlice(disableTplFlag.Name); len(disable) > 0 {
for _, templateName := range disable {
if ok := cfg.Templates.Remove(templateName); ok {
log.Info("Template disabled", logger.String("name", templateName))
}
}
}
// check if there are any templates available to render error pages
if len(cfg.Templates.Names()) == 0 {
return errors.New("no templates available to render error pages")
}
// if the rotation mode is set to random-on-startup, pick a random template (ignore the user-provided
// template name)
if cfg.RotationMode == config.RotationModeRandomOnStartup {
cfg.TemplateName = cfg.Templates.RandomName()
} else { // otherwise, use the user-provided template name
cfg.TemplateName = c.String(templateNameFlag.Name)
if !cfg.Templates.Has(cfg.TemplateName) {
return fmt.Errorf(
"template '%s' not found and cannot be used (available templates: %s)",
cfg.TemplateName,
cfg.Templates.Names(),
)
}
}
log.Debug("Configuration",
logger.Strings("loaded templates", cfg.Templates.Names()...),
logger.Strings("described HTTP codes", cfg.Codes.Codes()...),
logger.String("JSON format", cfg.Formats.JSON),
logger.String("XML format", cfg.Formats.XML),
logger.String("plain text format", cfg.Formats.PlainText),
logger.String("template name", cfg.TemplateName),
logger.Bool("disable localization", cfg.L10n.Disable),
logger.Uint16("default code to render", cfg.DefaultCodeToRender),
logger.Bool("respond with the same HTTP code", cfg.RespondWithSameHTTPCode),
logger.String("rotation mode", cfg.RotationMode.String()),
logger.Bool("show details", cfg.ShowDetails),
logger.Strings("proxy HTTP headers", cfg.ProxyHeaders...),
)
return cmd.Run(ctx, log, &cfg)
},
Flags: []cli.Flag{
&addrFlag,
&portFlag,
&addTplFlag,
&disableTplFlag,
&addCodeFlag,
&jsonFormatFlag,
&xmlFormatFlag,
&plainTextFormatFlag,
&templateNameFlag,
&disableL10nFlag,
&defaultCodeToRenderFlag,
&sendSameHTTPCodeFlag,
&showDetailsFlag,
&proxyHeadersListFlag,
&rotationModeFlag,
},
}
return cmd.c
}
// Run current command.
func (cmd *command) Run(ctx context.Context, log *logger.Logger, cfg *config.Config) error { //nolint:funlen
var srv = appHttp.NewServer(log)
if err := srv.Register(cfg); err != nil {
return err
}
startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
var startingErrCh = make(chan error, 1) // channel for server starting error
defer close(startingErrCh)
// to track the frequency of each template's use, we send a simple GET request to the GoatCounter API
// (https://goatcounter.com, https://github.com/arp242/goatcounter) to increment the counter. this service is
// free and does not require an API key. no private data is sent, as shown in the URL below. this is necessary
// to render a badge displaying the number of template usages on the error-pages repository README file :D
//
// badge code example:
// ![Used times](https://img.shields.io/badge/dynamic/json?
// url=https%3A%2F%2Ferror-pages.goatcounter.com%2Fcounter%2F%2Fuse-template%2Flost-in-space.json
// &query=%24.count&label=Used%20times)
//
// if you wish, you may view the collected statistics at any time here - https://error-pages.goatcounter.com/
go func() {
var tpl = url.QueryEscape(cfg.TemplateName)
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(
// https://www.goatcounter.com/help/pixel
"https://error-pages.goatcounter.com/count?p=/use-template/%s&t=%s", tpl, tpl,
), http.NoBody)
if reqErr != nil {
return
}
req.Header.Set("User-Agent", fmt.Sprintf("Mozilla/5.0 (error-pages, rnd:%d)", time.Now().UnixNano()))
resp, respErr := (&http.Client{Timeout: 10 * time.Second}).Do(req) //nolint:mnd // don't care about the response
if respErr != nil {
log.Debug("Cannot send a request to increment the template usage counter", logger.Error(respErr))
return
} else if resp != nil {
_ = resp.Body.Close()
}
}()
// start HTTP server in separate goroutine
go func(errCh chan<- error) {
defer close(errCh)
var now = time.Now()
log.Info("Server starting",
zap.String("addr", f.Listen.IP),
zap.Uint16("port", f.Listen.Port),
zap.String("default error page", opt.Default.PageCode),
zap.Uint16("default HTTP response code", opt.Default.HTTPCode),
zap.Strings("proxy headers", opt.ProxyHTTPHeaders),
zap.Bool("show request details", opt.ShowDetails),
zap.Bool("localization disabled", opt.L10n.Disabled),
defer func() {
log.Info("HTTP server stopped", logger.Duration("uptime", time.Since(now).Round(time.Millisecond)))
}()
log.Info("HTTP server starting",
logger.String("addr", cmd.opt.http.addr),
logger.Uint16("port", cmd.opt.http.port),
)
if err := server.Start(f.Listen.IP, f.Listen.Port); err != nil {
if err := srv.Start(cmd.opt.http.addr, cmd.opt.http.port); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
}(startingErrCh)
@ -144,16 +357,11 @@ func run(parentCtx context.Context, log *zap.Logger, cfg *config.Config, f flags
return err
case <-ctx.Done(): // ..or context cancellation
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
const shutdownTimeout = 5 * time.Second
if p, ok := picker.(interface{ Close() error }); ok {
if err := p.Close(); err != nil {
return err
}
}
log.Info("HTTP server stopping", logger.Duration("with timeout", shutdownTimeout))
// stop the server using created context above
if err := server.Stop(); err != nil {
if err := srv.Stop(shutdownTimeout); err != nil { //nolint:contextcheck
return err
}
}

View File

@ -1,7 +1,101 @@
package serve_test
import "testing"
import (
"context"
"fmt"
"net"
"strconv"
"testing"
"time"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
"github.com/stretchr/testify/require"
"gh.tarampamp.am/error-pages/internal/cli/serve"
"gh.tarampamp.am/error-pages/internal/logger"
)
func TestCommand_Run(t *testing.T) {
t.Parallel()
var (
port = getFreeTcpPort(t)
cmd = serve.NewCommand(logger.NewNop())
)
var ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var ch = make(chan error, 1)
go func() {
defer close(ch)
ch <- cmd.Run(ctx, []string{
"serve",
"--port", strconv.Itoa(int(port)),
"--add-template", "./testdata/foo-template.html",
"--disable-template", "ghost",
"--disable-template", "<unknown>",
"--add-http-code", "200=Code/Description",
"--json-format", "json format",
"--xml-format", "xml format",
"--plaintext-format", "plaintext format",
"--template-name", "foo-template",
"--disable-l10n",
"--default-error-page", "503",
"--send-same-http-code",
"--show-details",
"--proxy-headers", "X-Forwarded-For,X-Forwarded-Proto",
"--rotation-mode", "random-on-each-request",
})
}()
var connected bool
for {
conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second)
if err == nil {
connected = true
require.NoError(t, conn.Close())
break
} else {
t.Log(err)
}
select {
case <-ctx.Done():
t.Fatal("timeout")
case chErr := <-ch:
require.NoError(t, chErr)
case <-time.After(10 * time.Millisecond):
}
}
require.True(t, connected, "server is not running")
}
// getFreeTcpPort is a helper function to get a free TCP port number.
func getFreeTcpPort(t *testing.T) uint16 {
t.Helper()
l, lErr := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, lErr)
port := l.Addr().(*net.TCPAddr).Port
require.NoError(t, l.Close())
// make sure port is closed
for {
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
break
}
require.NoError(t, conn.Close())
<-time.After(5 * time.Millisecond)
}
return uint16(port)
}

View File

@ -1,235 +0,0 @@
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
}

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,134 @@
package shared
import (
"fmt"
"net"
"os"
"strings"
"github.com/urfave/cli/v3"
"gh.tarampamp.am/error-pages/internal/config"
)
// Note: Don't use pointers for flags, because they have own state which is not thread-safe.
// https://github.com/urfave/cli/issues/1926
var ListenAddrFlag = cli.StringFlag{
Name: "listen",
Aliases: []string{"l"},
Usage: "IP (v4 or v6) address to listen on",
Value: "0.0.0.0", // bind to all interfaces by default
Sources: cli.EnvVars("LISTEN_ADDR"),
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
Validator: func(ip string) error {
if ip == "" {
return fmt.Errorf("missing IP address")
}
if net.ParseIP(ip) == nil {
return fmt.Errorf("wrong IP address [%s] for listening", ip)
}
return nil
},
}
var ListenPortFlag = cli.UintFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "TCP port number",
Value: 8080, // default port number
Sources: cli.EnvVars("LISTEN_PORT"),
OnlyOnce: true,
Validator: func(port uint64) error {
if port == 0 || port > 65535 {
return fmt.Errorf("wrong TCP port number [%d]", port)
}
return nil
},
}
var AddTemplatesFlag = cli.StringSliceFlag{
Name: "add-template",
Usage: "to add a new template, provide the path to the file using this flag (the filename without the extension " +
"will be used as the template name)",
Config: cli.StringConfig{TrimSpace: true},
Validator: func(paths []string) error {
for _, path := range paths {
if path == "" {
return fmt.Errorf("missing template path")
}
if stat, err := os.Stat(path); err != nil || stat.IsDir() {
return fmt.Errorf("wrong template path [%s]", path)
}
}
return nil
},
}
var DisableTemplateNamesFlag = cli.StringSliceFlag{
Name: "disable-template",
Usage: "disable the specified template by its name (useful to disable the built-in templates and use only custom ones)",
Config: cli.StringConfig{TrimSpace: true},
}
var AddHTTPCodesFlag = cli.StringMapFlag{
Name: "add-http-code",
Aliases: []string{"add-code"},
Usage: "to add a new HTTP status code, provide the code and its message/description using this flag (the format " +
"should be '%code%=%message%/%description%'; the code may contain a wildcard '*' to cover multiple codes at " +
"once, for example, '4**' will cover all 4xx codes unless a more specific code is described previously)",
Config: cli.StringConfig{TrimSpace: true},
Validator: func(codes map[string]string) error {
for code, msgAndDesc := range codes {
if code == "" {
return fmt.Errorf("missing HTTP code")
} else if len(code) != 3 {
return fmt.Errorf("wrong HTTP code [%s]: it should be 3 characters long", code)
}
if parts := strings.SplitN(msgAndDesc, "/", 3); len(parts) < 1 || len(parts) > 2 {
return fmt.Errorf("wrong message/description format for HTTP code [%s]: %s", code, msgAndDesc)
} else if parts[0] == "" {
return fmt.Errorf("missing message for HTTP code [%s]", code)
}
}
return nil
},
}
// ParseHTTPCodes converts a map of HTTP status codes and their messages/descriptions into a map of codes and
// descriptions. Should be used together with [AddHTTPCodesFlag].
func ParseHTTPCodes(codes map[string]string) map[string]config.CodeDescription {
var result = make(map[string]config.CodeDescription, len(codes))
for code, msgAndDesc := range codes {
var (
parts = strings.SplitN(msgAndDesc, "/", 2)
desc config.CodeDescription
)
desc.Message = strings.TrimSpace(parts[0])
if len(parts) > 1 {
desc.Description = strings.TrimSpace(parts[1])
}
result[code] = desc
}
return result
}
var DisableL10nFlag = cli.BoolFlag{
Name: "disable-l10n",
Usage: "disable localization of error pages (if the template supports localization)",
Sources: cli.EnvVars("DISABLE_L10N"),
OnlyOnce: true,
}

View File

@ -0,0 +1,218 @@
package shared_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/config"
)
func TestListenAddrFlag(t *testing.T) {
t.Parallel()
var flag = shared.ListenAddrFlag
assert.Equal(t, "listen", flag.Name)
assert.Equal(t, "0.0.0.0", flag.Value)
assert.Contains(t, flag.Sources.String(), "LISTEN_ADDR")
for giveValue, wantErrMsg := range map[string]string{
flag.Value: "", // default value
// ipv4
"0.0.0.0": "",
"127.0.0.1": "",
"255.255.255.255": "",
// ipv6
"::": "",
"::1": "",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334": "",
"2001:db8:85a3:0:0:8a2e:370:7334": "",
"2001:db8:85a3::8a2e:370:7334": "",
"2001:db8::8a2e:370:7334": "",
"2001:db8::7334": "",
"2001:db8::": "",
"2001:db8:0:0:1::1": "",
"2001:db8:0:0:1::": "",
// invalid
"": "missing IP address",
"255.255.255.256": "wrong IP address [255.255.255.256] for listening",
"example.com": "wrong IP address [example.com] for listening",
"123.123.abc.123": "wrong IP address [123.123.abc.123] for listening",
"foo:123:321": "wrong IP address [foo:123:321] for listening",
"2001:db8:0:0:1:": "wrong IP address [2001:db8:0:0:1:] for listening",
} {
t.Run(fmt.Sprintf("%s: %s", giveValue, wantErrMsg), func(t *testing.T) {
if err := flag.Validator(giveValue); wantErrMsg != "" {
assert.ErrorContains(t, err, wantErrMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestListenPortFlag(t *testing.T) {
t.Parallel()
var flag = shared.ListenPortFlag
assert.Equal(t, "port", flag.Name)
assert.Equal(t, uint64(8080), flag.Value)
assert.Contains(t, flag.Sources.String(), "LISTEN_PORT")
for giveValue, wantErrMsg := range map[uint64]string{
flag.Value: "", // default value
1: "",
8080: "",
65535: "",
0: "wrong TCP port number [0]",
65536: "wrong TCP port number [65536]",
} {
t.Run(fmt.Sprintf("%d: %s", giveValue, wantErrMsg), func(t *testing.T) {
if err := flag.Validator(giveValue); wantErrMsg != "" {
assert.ErrorContains(t, err, wantErrMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestAddTemplatesFlag(t *testing.T) {
t.Parallel()
var flag = shared.AddTemplatesFlag
assert.Equal(t, "add-template", flag.Name)
for wantErrMsg, giveValue := range map[string][]string{
"missing template path": {""},
"wrong template path [.]": {".", "./"},
"wrong template path [..]": {"..", "../"},
"wrong template path [foo]": {"foo"},
"": {"./flags.go"},
} {
t.Run(fmt.Sprintf("%s: %s", giveValue, wantErrMsg), func(t *testing.T) {
if err := flag.Validator(giveValue); wantErrMsg != "" {
assert.ErrorContains(t, err, wantErrMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestDisableTemplateNamesFlag(t *testing.T) {
t.Parallel()
var flag = shared.DisableTemplateNamesFlag
assert.Equal(t, "disable-template", flag.Name)
}
func TestAddHTTPCodesFlag(t *testing.T) {
t.Parallel()
var flag = shared.AddHTTPCodesFlag
assert.Equal(t, "add-http-code", flag.Name)
for name, tt := range map[string]struct {
giveValue map[string]string
wantErrMsg string
}{
"common": {
giveValue: map[string]string{
"200": "foo/bar",
"404": "foo",
"2**": "baz",
},
},
"missing HTTP code": {
giveValue: map[string]string{"": "foo/bar"},
wantErrMsg: "missing HTTP code",
},
"wrong HTTP code [6]": {
giveValue: map[string]string{"6": "foo"},
wantErrMsg: "wrong HTTP code [6]: it should be 3 characters long",
},
"wrong HTTP code [66]": {
giveValue: map[string]string{"66": "foo"},
wantErrMsg: "wrong HTTP code [66]: it should be 3 characters long",
},
"wrong HTTP code [1000]": {
giveValue: map[string]string{"1000": "foo"},
wantErrMsg: "wrong HTTP code [1000]: it should be 3 characters long",
},
"missing message and description": {
giveValue: map[string]string{"200": "//"},
wantErrMsg: "wrong message/description format for HTTP code [200]: //",
},
"missing message": {
giveValue: map[string]string{"200": "/bar"},
wantErrMsg: "missing message for HTTP code [200]",
},
} {
t.Run(name, func(t *testing.T) {
if err := flag.Validator(tt.giveValue); tt.wantErrMsg != "" {
assert.ErrorContains(t, err, tt.wantErrMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestParseHTTPCodes(t *testing.T) {
t.Parallel()
assert.Equal(t, shared.ParseHTTPCodes(nil), map[string]config.CodeDescription{})
assert.Equal(t,
shared.ParseHTTPCodes(map[string]string{"200": "msg"}),
map[string]config.CodeDescription{"200": {Message: "msg", Description: ""}},
)
assert.Equal(t,
shared.ParseHTTPCodes(map[string]string{"200": "/aaa"}),
map[string]config.CodeDescription{"200": {Message: "", Description: "aaa"}},
)
assert.Equal(t, // not sure here
shared.ParseHTTPCodes(map[string]string{"aa": "////aaa"}),
map[string]config.CodeDescription{"aa": {Message: "", Description: "///aaa"}},
)
assert.Equal(t,
shared.ParseHTTPCodes(map[string]string{"200": "msg/desc"}),
map[string]config.CodeDescription{"200": {Message: "msg", Description: "desc"}},
)
assert.Equal(t,
shared.ParseHTTPCodes(map[string]string{
"200": "msg/desc",
"foo": "Word word/Desc desc // adsadas",
}),
map[string]config.CodeDescription{
"200": {Message: "msg", Description: "desc"},
"foo": {Message: "Word word", Description: "Desc desc // adsadas"},
},
)
}
func TestDisableL10nFlag(t *testing.T) {
t.Parallel()
var flag = shared.DisableL10nFlag
assert.Equal(t, "disable-l10n", flag.Name)
assert.Contains(t, flag.Sources.String(), "DISABLE_L10N")
}

View File

@ -0,0 +1,24 @@
//go:build ignore
// +build ignore
package main
import (
"os"
cliDocs "github.com/urfave/cli-docs/v3"
"gh.tarampamp.am/error-pages/internal/cli"
)
func main() {
const readmePath = "../../README.md"
if stat, err := os.Stat(readmePath); err == nil && stat.Mode().IsRegular() {
if err = cliDocs.ToTabularToFileBetweenTags(cli.NewApp(""), "error-pages", readmePath); err != nil {
panic(err)
}
} else if err != nil {
println("readme file not found, cli docs not updated:", err.Error())
}
}

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

124
internal/config/codes.go Normal file
View File

@ -0,0 +1,124 @@
package config
import (
"slices"
"strconv"
)
type (
CodeDescription struct {
// Message is a short description of the HTTP error.
Message string
// Description is a longer description of the HTTP error.
Description string
}
// Codes is a map of HTTP codes to their descriptions.
//
// The codes may be written in a non-strict manner. For example, they may be "4xx", "4XX", or "4**".
// If the map contains both "404" and "4xx" keys, and we search for "404", the "404" key will be returned.
// However, if we search for "405", "400", or any non-existing code that starts with "4" and its length is 3,
// the value under the key "4xx" will be retrieved.
//
// The length of the code (in string format) is matter.
Codes map[string]CodeDescription // map[http_code]description
)
// Find searches the closest match for the given HTTP code, written in a non-strict manner. Read [Codes] for more
// information.
func (c Codes) Find(httpCode uint16) (CodeDescription, bool) { //nolint:funlen,gocyclo
if len(c) == 0 { // empty map, fast return
return CodeDescription{}, false
}
var code = strconv.FormatUint(uint64(httpCode), 10)
if desc, ok := c[code]; ok { // search for the exact match
return desc, true
}
var (
keysMap = make(map[string][]rune, len(c))
codeRunes = []rune(code)
)
for key := range c { // take only the keys that are the same length and start with the same character or a wildcard
if kr := []rune(key); len(kr) > 0 && len(kr) == len(codeRunes) && isWildcardOr(kr[0], codeRunes[0]) {
keysMap[key] = kr
}
}
if len(keysMap) == 0 { // no matches found using the first rune comparison
return CodeDescription{}, false
}
var matchedMap = make(map[string]uint16, len(keysMap)) // map[mapKey]wildcardMatchedCount
for mapKey, keyRunes := range keysMap { // search for the closest match
var wildcardMatchedCount uint16 = 0
for i := 0; i < len(codeRunes); i++ { // loop through each httpCode rune
var keyRune, codeRune = keyRunes[i], codeRunes[i]
if wm := isWildcard(keyRune); wm || keyRune == codeRune {
if wm {
wildcardMatchedCount++
}
if i == len(codeRunes)-1 { // is the last rune?
matchedMap[mapKey] = wildcardMatchedCount
}
continue
}
break
}
}
if len(matchedMap) == 0 { // no matches found
return CodeDescription{}, false
} else if len(matchedMap) == 1 { // only one match found
for mapKey := range matchedMap {
return c[mapKey], true
}
}
// multiple matches found, find the most specific one based on the wildcard matched count (pick the one with the
// least wildcards)
var (
minCount uint16
key string
)
for mapKey, count := range matchedMap {
if minCount == 0 || count < minCount {
minCount, key = count, mapKey
}
}
return c[key], true
}
func isWildcard(r rune) bool { return r == '*' || r == 'x' || r == 'X' }
func isWildcardOr(r, or rune) bool { return isWildcard(r) || r == or }
// Codes returns all HTTP codes sorted alphabetically.
func (c Codes) Codes() []string {
var codes = make([]string, 0, len(c))
for code := range c {
codes = append(codes, code)
}
slices.Sort(codes)
return codes
}
// Has checks if the HTTP code exists.
func (c Codes) Has(code string) (found bool) { _, found = c[code]; return } //nolint:nlreturn
// Get returns the HTTP code description by the specified code, if it exists.
func (c Codes) Get(code string) (data CodeDescription, ok bool) { data, ok = c[code]; return } //nolint:nlreturn

View File

@ -0,0 +1,131 @@
package config_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gh.tarampamp.am/error-pages/internal/config"
)
func TestCodes_Common(t *testing.T) {
t.Parallel()
var codes = make(config.Codes)
t.Run("initial state", func(t *testing.T) {
require.Empty(t, codes.Codes())
require.Empty(t, codes.Has("404"))
var got, ok = codes.Get("404")
require.Empty(t, got)
require.False(t, ok)
})
t.Run("add a code", func(t *testing.T) {
codes["404"] = config.CodeDescription{Message: "Not Found"}
assert.True(t, codes.Has("404"))
assert.Equal(t, []string{"404"}, codes.Codes())
var got, ok = codes.Get("404")
assert.Equal(t, got.Message, "Not Found")
assert.True(t, ok)
})
}
func TestCodes_Find(t *testing.T) {
t.Parallel()
//nolint:typecheck
var common = config.Codes{
"101": {Message: "Upgrade"}, // 101
"1xx": {Message: "Informational"}, // 102-199
"200": {Message: "OK"}, // 200
"20*": {Message: "Success"}, // 201-209
"2**": {Message: "Success, but..."}, // 210-299
"3**": {Message: "Redirection"}, // 300-399
"404": {Message: "Not Found"}, // 404
"405": {Message: "Method Not Allowed"}, // 405
"500": {Message: "Internal Server Error"}, // 500
"501": {Message: "Not Implemented"}, // 501
"502": {Message: "Bad Gateway"}, // 502
"503": {Message: "Service Unavailable"}, // 503
"5XX": {Message: "Server Error"}, // 504-599
}
var ladder = config.Codes{
"123": {Message: "Full triple"},
"***": {Message: "Triple"},
"12": {Message: "Full double"},
"**": {Message: "Double"},
"1": {Message: "Full single"},
"*": {Message: "Single"},
}
for name, tt := range map[string]struct {
giveCodes config.Codes
giveCode uint16
wantMessage string
wantNotFound bool
}{
"101 - exact match": {giveCodes: common, giveCode: 101, wantMessage: "Upgrade"},
"102 - multi-wildcard match": {giveCodes: common, giveCode: 102, wantMessage: "Informational"},
"110 - multi-wildcard match": {giveCodes: common, giveCode: 110, wantMessage: "Informational"},
"111 - multi-wildcard match": {giveCodes: common, giveCode: 111, wantMessage: "Informational"},
"199 - multi-wildcard match": {giveCodes: common, giveCode: 199, wantMessage: "Informational"},
"200 - exact match": {giveCodes: common, giveCode: 200, wantMessage: "OK"},
"201 - single-wildcard match": {giveCodes: common, giveCode: 201, wantMessage: "Success"},
"209 - single-wildcard match": {giveCodes: common, giveCode: 209, wantMessage: "Success"},
"210 - multi-wildcard match": {giveCodes: common, giveCode: 210, wantMessage: "Success, but..."},
"234 - multi-wildcard match": {giveCodes: common, giveCode: 234, wantMessage: "Success, but..."},
"299 - multi-wildcard match": {giveCodes: common, giveCode: 299, wantMessage: "Success, but..."},
"300 - multi-wildcard match": {giveCodes: common, giveCode: 300, wantMessage: "Redirection"},
"301 - multi-wildcard match": {giveCodes: common, giveCode: 301, wantMessage: "Redirection"},
"311 - multi-wildcard match": {giveCodes: common, giveCode: 311, wantMessage: "Redirection"},
"399 - multi-wildcard match": {giveCodes: common, giveCode: 399, wantMessage: "Redirection"},
"400 - not found": {giveCodes: common, giveCode: 400, wantNotFound: true},
"403 - not found": {giveCodes: common, giveCode: 403, wantNotFound: true},
"404 - exact match": {giveCodes: common, giveCode: 404, wantMessage: "Not Found"},
"405 - exact match": {giveCodes: common, giveCode: 405, wantMessage: "Method Not Allowed"},
"410 - not found": {giveCodes: common, giveCode: 410, wantNotFound: true},
"450 - not found": {giveCodes: common, giveCode: 450, wantNotFound: true},
"499 - not found": {giveCodes: common, giveCode: 499, wantNotFound: true},
"500 - exact match": {giveCodes: common, giveCode: 500, wantMessage: "Internal Server Error"},
"501 - exact match": {giveCodes: common, giveCode: 501, wantMessage: "Not Implemented"},
"502 - exact match": {giveCodes: common, giveCode: 502, wantMessage: "Bad Gateway"},
"503 - exact match": {giveCodes: common, giveCode: 503, wantMessage: "Service Unavailable"},
"504 - multi-wildcard match": {giveCodes: common, giveCode: 504, wantMessage: "Server Error"},
"505 - multi-wildcard match": {giveCodes: common, giveCode: 505, wantMessage: "Server Error"},
"599 - multi-wildcard match": {giveCodes: common, giveCode: 599, wantMessage: "Server Error"},
"600 - not found": {giveCodes: common, giveCode: 600, wantNotFound: true},
"ladder - strict triple match": {giveCodes: ladder, giveCode: 123, wantMessage: "Full triple"},
"ladder - triple wildcard": {giveCodes: ladder, giveCode: 321, wantMessage: "Triple"},
"ladder - strict double match": {giveCodes: ladder, giveCode: 12, wantMessage: "Full double"},
"ladder - double wildcard": {giveCodes: ladder, giveCode: 21, wantMessage: "Double"},
"ladder - strict single match": {giveCodes: ladder, giveCode: 1, wantMessage: "Full single"},
"ladder - single wildcard": {giveCodes: ladder, giveCode: 2, wantMessage: "Single"},
"empty map": {giveCodes: config.Codes{}, giveCode: 404, wantNotFound: true},
"zero code": {giveCodes: common, giveCode: 0, wantNotFound: true},
} {
t.Run(name, func(t *testing.T) {
for i := 0; i < 100; i++ { // repeat the test to ensure the function is idempotent
var desc, found = tt.giveCodes.Find(tt.giveCode)
if !tt.wantNotFound {
require.Truef(t, found, "should have found something")
require.Equal(t, tt.wantMessage, desc.Message)
} else {
require.Falsef(t, found, "should not have found anything, but got: %v", desc)
require.Empty(t, desc)
}
}
})
}
}

View File

@ -1,256 +1,175 @@
package config
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"maps"
"net/http"
"slices"
"github.com/a8m/envsubst"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
builtinTemplates "gh.tarampamp.am/error-pages/templates"
)
// Config is a main (exportable) config struct.
type Config struct {
Templates []Template
Pages map[string]Page // map key is a page code
Formats map[string]Format // map key is a format name
// Templates hold all templates, with the key being the template name and the value being the template content
// in HTML format (Go templates are supported here).
Templates templates
// Formats contain alternative response formats (e.g., if a client requests a response in one of these formats,
// we will render the response using the specified format instead of HTML; Go templates are supported).
Formats struct {
JSON string
XML string
PlainText string
}
// Codes hold descriptions for HTTP codes (e.g., 404: "Not Found / The server can not find the requested page").
Codes Codes
// TemplateName is the name of the template to use for rendering error pages. The template must be present in the
// Templates map.
TemplateName string
// ProxyHeaders contains a list of HTTP headers that will be proxied from the incoming request to the
// error page response.
ProxyHeaders []string
// L10n contains localization settings.
L10n struct {
// Disable the localization of error pages.
Disable bool
}
// DefaultCodeToRender is the code for the default error page to be displayed. It is used when the requested
// code is not defined in the incoming request (i.e., the code to render as the index page).
DefaultCodeToRender uint16
// RespondWithSameHTTPCode determines whether the response should have the same HTTP status code as the requested
// error page.
// In other words, if set to true and the requested error page has a code of 404, the HTTP response will also have
// a status code of 404. If set to false, the HTTP response will have a status code of 200 regardless of the
// requested error page's status code.
RespondWithSameHTTPCode bool
// RotationMode allows to set the rotation mode for templates to switch between them automatically on startup,
// on each request, daily, hourly and so on.
RotationMode RotationMode
// ShowDetails determines whether to show additional details in the error response, extracted from the
// incoming request (if supported by the template).
ShowDetails bool
}
// Template returns a Template with the passes name.
func (c *Config) Template(name string) (*Template, bool) {
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].name == name {
return &c.Templates[i], true
}
}
const defaultJSONFormat string = `{
"error": true,
"code": {{ code | json }},
"message": {{ message | json }},
"description": {{ description | json }}{{ if show_details }},
"details": {
"host": {{ host | json }},
"original_uri": {{ original_uri | json }},
"forwarded_for": {{ forwarded_for | json }},
"namespace": {{ namespace | json }},
"ingress_name": {{ ingress_name | json }},
"service_name": {{ service_name | json }},
"service_port": {{ service_port | json }},
"request_id": {{ request_id | json }},
"timestamp": {{ now.Unix }}
}{{ end }}
}
` // an empty line at the end is important for better UX
return &Template{}, false
const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
<error>
<code>{{ code }}</code>
<message>{{ message }}</message>
<description>{{ description }}</description>{{ if show_details }}
<details>
<host>{{ host }}</host>
<originalURI>{{ original_uri }}</originalURI>
<forwardedFor>{{ forwarded_for }}</forwardedFor>
<namespace>{{ namespace }}</namespace>
<ingressName>{{ ingress_name }}</ingressName>
<serviceName>{{ service_name }}</serviceName>
<servicePort>{{ service_port }}</servicePort>
<requestID>{{ request_id }}</requestID>
<timestamp>{{ now.Unix }}</timestamp>
</details>{{ end }}
</error>
` // an empty line at the end is important for better UX
const defaultPlainTextFormat string = `Error {{ code }}: {{ message }}{{ if description }}
{{ description }}{{ end }}{{ if show_details }}
Host: {{ host }}
Original URI: {{ original_uri }}
Forwarded For: {{ forwarded_for }}
Namespace: {{ namespace }}
Ingress Name: {{ ingress_name }}
Service Name: {{ service_name }}
Service Port: {{ service_port }}
Request ID: {{ request_id }}
Timestamp: {{ now.Unix }}{{ end }}
` // an empty line at the end is important for better UX
//nolint:lll
var defaultCodes = Codes{ //nolint:gochecknoglobals
"400": {"Bad Request", "The server did not understand the request"},
"401": {"Unauthorized", "The requested page needs a username and a password"},
"403": {"Forbidden", "Access is forbidden to the requested page"},
"404": {"Not Found", "The server can not find the requested page"},
"405": {"Method Not Allowed", "The method specified in the request is not allowed"},
"407": {"Proxy Authentication Required", "You must authenticate with a proxy server before this request can be served"},
"408": {"Request Timeout", "The request took longer than the server was prepared to wait"},
"409": {"Conflict", "The request could not be completed because of a conflict"},
"410": {"Gone", "The requested page is no longer available"},
"411": {"Length Required", "The \"Content-Length\" is not defined. The server will not accept the request without it"},
"412": {"Precondition Failed", "The pre condition given in the request evaluated to false by the server"},
"413": {"Payload Too Large", "The server will not accept the request, because the request entity is too large"},
"416": {"Requested Range Not Satisfiable", "The requested byte range is not available and is out of bounds"},
"418": {"I'm a teapot", "Attempt to brew coffee with a teapot is not supported"},
"429": {"Too Many Requests", "Too many requests in a given amount of time"},
"500": {"Internal Server Error", "The server met an unexpected condition"},
"502": {"Bad Gateway", "The server received an invalid response from the upstream server"},
"503": {"Service Unavailable", "The server is temporarily overloading or down"},
"504": {"Gateway Timeout", "The gateway has timed out"},
"505": {"HTTP Version Not Supported", "The server does not support the \"http protocol\" version"},
}
func (c *Config) JSONFormat() (*Format, bool) { return c.format("json") }
func (c *Config) XMLFormat() (*Format, bool) { return c.format("xml") }
func (c *Config) format(name string) (*Format, bool) {
if f, ok := c.Formats[name]; ok {
if len(f.content) > 0 {
return &f, true
}
}
return &Format{}, false
var defaultProxyHeaders = []string{ //nolint:gochecknoglobals
// "Traceparent", // W3C Trace Context
// "Tracestate", // W3C Trace Context
"X-Request-Id", // unofficial HTTP header, used to trace individual HTTP requests
"X-Trace-Id", // same as above
"X-Amzn-Trace-Id", // to track HTTP requests from clients to targets or other AWS services
}
// TemplateNames returns all template names.
func (c *Config) TemplateNames() []string {
n := make([]string, len(c.Templates))
for i, t := range c.Templates {
n[i] = t.name
// New creates a new configuration with default values.
func New() Config {
var cfg = Config{
Templates: make(templates), // allocate memory for templates
Codes: maps.Clone(defaultCodes), // copy default codes
}
return n
}
// Template describes HTTP error page template.
type Template struct {
name string
content []byte
}
// Name returns the name of the template.
func (t Template) Name() string { return t.name }
// Content returns the template content.
func (t Template) Content() []byte { return t.content }
func (t *Template) loadContentFromFile(filePath string) (err error) {
if t.content, err = ioutil.ReadFile(filePath); err != nil {
return errors.Wrap(err, "cannot load content for the template "+t.Name()+" from file "+filePath)
}
return
}
// Page describes error page.
type Page struct {
code string
message string
description string
}
// Code returns the code of the Page.
func (p Page) Code() string { return p.code }
// Message returns the message of the Page.
func (p Page) Message() string { return p.message }
// Description returns the description of the Page.
func (p Page) Description() string { return p.description }
// Format describes different response formats.
type Format struct {
name string
content []byte
}
// Name returns the name of the format.
func (f Format) Name() string { return f.name }
// Content returns the format content.
func (f Format) Content() []byte { return f.content }
// config is internal struct for marshaling/unmarshaling configuration file content.
type config struct {
Templates []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
} `yaml:"templates"`
Formats map[string]struct {
Content string `yaml:"content"`
} `yaml:"formats"`
Pages map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
} `yaml:"pages"`
}
// Validate the config struct and return an error if something is wrong.
func (c config) Validate() error {
if len(c.Templates) == 0 {
return errors.New("empty templates list")
} else {
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].Name == "" && c.Templates[i].Path == "" {
return errors.New("empty path and name with index " + strconv.Itoa(i))
}
if c.Templates[i].Path == "" && c.Templates[i].Content == "" {
return errors.New("empty path and template content with index " + strconv.Itoa(i))
}
}
}
if len(c.Pages) == 0 {
return errors.New("empty pages list")
} else {
for code := range c.Pages {
if code == "" {
return errors.New("empty page code")
}
if strings.ContainsRune(code, ' ') {
return errors.New("code should not contain whitespaces")
}
}
}
if len(c.Formats) > 0 {
for name := range c.Formats {
if name == "" {
return errors.New("empty format name")
}
if strings.ContainsRune(name, ' ') {
return errors.New("format should not contain whitespaces")
}
}
}
return nil
}
// Export the config struct into Config.
func (c *config) Export() (*Config, error) {
cfg := &Config{}
cfg.Templates = make([]Template, 0, len(c.Templates))
for i := 0; i < len(c.Templates); i++ {
tpl := Template{name: c.Templates[i].Name}
if c.Templates[i].Content == "" {
if c.Templates[i].Path == "" {
return nil, errors.New("path to the template " + c.Templates[i].Name + " not provided")
}
if err := tpl.loadContentFromFile(c.Templates[i].Path); err != nil {
return nil, err
}
} else {
tpl.content = []byte(c.Templates[i].Content)
}
cfg.Templates = append(cfg.Templates, tpl)
}
cfg.Pages = make(map[string]Page, len(c.Pages))
for code, p := range c.Pages {
cfg.Pages[code] = Page{code: code, message: p.Message, description: p.Description}
}
cfg.Formats = make(map[string]Format, len(c.Formats))
for name, f := range c.Formats {
cfg.Formats[name] = Format{name: name, content: []byte(strings.TrimSpace(f.Content))}
}
return cfg, nil
}
// FromYaml creates new Config instance using YAML-structured content.
func FromYaml(in []byte) (_ *Config, err error) {
in, err = envsubst.Bytes(in)
if err != nil {
return nil, err
}
c := &config{}
if err = yaml.Unmarshal(in, c); err != nil {
return nil, errors.Wrap(err, "cannot parse configuration file")
}
var basename string
for i := 0; i < len(c.Templates); i++ {
if c.Templates[i].Name == "" { // set the template name from file path
basename = filepath.Base(c.Templates[i].Path)
c.Templates[i].Name = strings.TrimSuffix(basename, filepath.Ext(basename))
}
}
if err = c.Validate(); err != nil {
return nil, err
}
return c.Export()
}
// FromYamlFile creates new Config instance using YAML file.
func FromYamlFile(filepath string) (*Config, error) {
bytes, err := ioutil.ReadFile(filepath)
if err != nil {
return nil, errors.Wrap(err, "cannot read configuration file")
}
// the following code makes it possible to use the relative links in the config file (`.` means "directory with
// the config file")
cwd, err := os.Getwd()
if err == nil {
if err = os.Chdir(path.Dir(filepath)); err != nil {
return nil, err
}
defer func() { _ = os.Chdir(cwd) }()
}
return FromYaml(bytes)
cfg.Formats.JSON = defaultJSONFormat
cfg.Formats.XML = defaultXMLFormat
cfg.Formats.PlainText = defaultPlainTextFormat
// add built-in templates
for name, content := range builtinTemplates.BuiltIn() {
cfg.Templates[name] = content
}
// set first template as default
for _, name := range cfg.Templates.Names() {
cfg.TemplateName = name
break
}
// set default HTTP headers to proxy
cfg.ProxyHeaders = slices.Clone(defaultProxyHeaders)
// set defaults
cfg.DefaultCodeToRender = http.StatusNotFound
return cfg
}

View File

@ -1,195 +1,57 @@
package config_test
import (
"os"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/template"
)
func TestFromYaml(t *testing.T) {
var cases = map[string]struct { //nolint:maligned
giveYaml []byte
giveEnv map[string]string
wantErr bool
checkResultFn func(*testing.T, *config.Config)
}{
"with all possible values": {
giveEnv: map[string]string{
"__FOO_TPL_PATH": "./testdata/foo-tpl.html",
"__FOO_TPL_NAME": "Foo Template",
},
giveYaml: []byte(`
templates:
- path: ${__FOO_TPL_PATH}
name: ${__FOO_TPL_NAME:-default_value} # name is optional
- path: ./testdata/bar-tpl.html
- name: Baz
content: |
Some content {{ code }}
New line
func TestNew(t *testing.T) {
t.Parallel()
formats:
json:
content: |
{"code": "{{code}}"}
Avada_Kedavra:
content: "{{ message }}"
t.Run("default config", func(t *testing.T) {
var cfg = config.New()
pages:
400:
message: Bad Request
description: The server did not understand the request
assert.NotEmpty(t, cfg.Formats.XML)
assert.NotEmpty(t, cfg.Formats.JSON)
assert.NotEmpty(t, cfg.Formats.PlainText)
assert.True(t, len(cfg.Codes) >= 19)
assert.True(t, len(cfg.Templates) >= 1)
assert.NotEmpty(t, cfg.TemplateName)
assert.True(t, cfg.Templates.Has(cfg.TemplateName))
assert.Equal(t, uint16(http.StatusNotFound), cfg.DefaultCodeToRender)
})
401:
message: Unauthorized
description: The requested page needs a username and a password
`),
wantErr: false,
checkResultFn: func(t *testing.T, cfg *config.Config) {
assert.Len(t, cfg.Templates, 3)
t.Run("changing cfg1 should not affect cfg2", func(t *testing.T) {
var cfg1, cfg2 = config.New(), config.New()
tpl, found := cfg.Template("Foo Template")
assert.True(t, found)
assert.Equal(t, "Foo Template", tpl.Name())
assert.Equal(t, "<html><body>foo {{ code }}</body></html>\n", string(tpl.Content()))
cfg1.Codes["400"] = config.CodeDescription{Message: "foo", Description: "bar"}
tpl, found = cfg.Template("bar-tpl")
assert.True(t, found)
assert.Equal(t, "bar-tpl", tpl.Name())
assert.Equal(t, "<html><body>bar {{ code }}</body></html>\n", string(tpl.Content()))
assert.NotEqual(t, cfg1.Codes["400"], cfg2.Codes["400"])
tpl, found = cfg.Template("Baz")
assert.True(t, found)
assert.Equal(t, "Baz", tpl.Name())
assert.Equal(t, "Some content {{ code }}\nNew line\n", string(tpl.Content()))
cfg1.ProxyHeaders = append(cfg1.ProxyHeaders, "foo")
tpl, found = cfg.Template("NonExists")
assert.False(t, found)
assert.Equal(t, "", tpl.Name())
assert.Equal(t, "", string(tpl.Content()))
assert.NotEqual(t, cfg1.ProxyHeaders, cfg2.ProxyHeaders)
})
assert.Len(t, cfg.Formats, 2)
t.Run("render default format templates", func(t *testing.T) {
var cfg = config.New()
format, found := cfg.Formats["json"]
assert.True(t, found)
assert.Equal(t, `{"code": "{{code}}"}`, string(format.Content()))
for _, content := range []string{cfg.Formats.JSON, cfg.Formats.XML, cfg.Formats.PlainText} {
var result, err = template.Render(content, template.Props{
ShowRequestDetails: true,
Code: 404,
Message: "Not Found",
})
format, found = cfg.Formats["Avada_Kedavra"]
assert.True(t, found)
assert.Equal(t, "{{ message }}", string(format.Content()))
assert.NotEmpty(t, result)
assert.NoError(t, err)
assert.Len(t, cfg.Pages, 2)
errPage, found := cfg.Pages["400"]
assert.True(t, found)
assert.Equal(t, "400", errPage.Code())
assert.Equal(t, "Bad Request", errPage.Message())
assert.Equal(t, "The server did not understand the request", errPage.Description())
errPage, found = cfg.Pages["401"]
assert.True(t, found)
assert.Equal(t, "401", errPage.Code())
assert.Equal(t, "Unauthorized", errPage.Message())
assert.Equal(t, "The requested page needs a username and a password", errPage.Description())
errPage, found = cfg.Pages["666"]
assert.False(t, found)
assert.Equal(t, "", errPage.Message())
assert.Equal(t, "", errPage.Code())
assert.Equal(t, "", errPage.Description())
},
},
"broken yaml": {
giveYaml: []byte(`foo bar`),
wantErr: true,
},
}
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
if tt.giveEnv != nil {
for key, value := range tt.giveEnv {
assert.NoError(t, os.Setenv(key, value))
}
}
conf, err := config.FromYaml(tt.giveYaml)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.Nil(t, err)
tt.checkResultFn(t, conf)
}
if tt.giveEnv != nil {
for key := range tt.giveEnv {
assert.NoError(t, os.Unsetenv(key))
}
}
})
}
}
func TestFromYamlFile(t *testing.T) {
var cases = map[string]struct { //nolint:maligned
giveYamlFilePath string
wantErr bool
checkResultFn func(*testing.T, *config.Config)
}{
"with all possible values": {
giveYamlFilePath: "./testdata/simple.yml",
wantErr: false,
checkResultFn: func(t *testing.T, cfg *config.Config) {
assert.Len(t, cfg.Templates, 2)
tpl, found := cfg.Template("ghost")
assert.True(t, found)
assert.Equal(t, "ghost", tpl.Name())
assert.Equal(t, "<html><body>foo {{ code }}</body></html>\n", string(tpl.Content()))
tpl, found = cfg.Template("bar-tpl")
assert.True(t, found)
assert.Equal(t, "bar-tpl", tpl.Name())
assert.Equal(t, "<html><body>bar {{ code }}</body></html>\n", string(tpl.Content()))
assert.Len(t, cfg.Pages, 2)
errPage, found := cfg.Pages["400"]
assert.True(t, found)
assert.Equal(t, "400", errPage.Code())
assert.Equal(t, "Bad Request", errPage.Message())
assert.Equal(t, "The server did not understand the request", errPage.Description())
errPage, found = cfg.Pages["401"]
assert.True(t, found)
assert.Equal(t, "401", errPage.Code())
assert.Equal(t, "Unauthorized", errPage.Message())
assert.Equal(t, "The requested page needs a username and a password", errPage.Description())
},
},
"broken yaml": {
giveYamlFilePath: "./testdata/broken.yml",
wantErr: true,
},
"wrong file path": {
giveYamlFilePath: "foo bar",
wantErr: true,
},
}
for name, tt := range cases {
t.Run(name, func(t *testing.T) {
conf, err := config.FromYamlFile(tt.giveYamlFilePath)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.Nil(t, err)
tt.checkResultFn(t, conf)
}
})
}
t.Log(result)
}
})
}

View File

@ -0,0 +1,87 @@
package config
import (
"fmt"
"strings"
)
// RotationMode represents the rotation mode for templates.
type RotationMode byte
const (
RotationModeDisabled RotationMode = iota // do not rotate templates, default
RotationModeRandomOnStartup // pick a random template on startup
RotationModeRandomOnEachRequest // pick a random template on each request
RotationModeRandomHourly // once an hour switch to a random template
RotationModeRandomDaily // once a day switch to a random template
)
// String returns a human-readable representation of the rotation mode.
func (rm RotationMode) String() string {
switch rm {
case RotationModeDisabled:
return "disabled"
case RotationModeRandomOnStartup:
return "random-on-startup"
case RotationModeRandomOnEachRequest:
return "random-on-each-request"
case RotationModeRandomHourly:
return "random-hourly"
case RotationModeRandomDaily:
return "random-daily"
}
return fmt.Sprintf("RotationMode(%d)", rm)
}
// RotationModes returns a slice of all rotation modes.
func RotationModes() []RotationMode {
return []RotationMode{
RotationModeDisabled,
RotationModeRandomOnStartup,
RotationModeRandomOnEachRequest,
RotationModeRandomHourly,
RotationModeRandomDaily,
}
}
// RotationModeStrings returns a slice of all rotation modes as strings.
func RotationModeStrings() []string {
var (
modes = RotationModes()
result = make([]string, len(modes))
)
for i := range modes {
result[i] = modes[i].String()
}
return result
}
// ParseRotationMode parses a rotation mode (case is ignored) based on the ASCII representation of the rotation mode.
// If the provided ASCII representation is invalid an error is returned.
func ParseRotationMode[T string | []byte](text T) (RotationMode, error) {
var mode string
if s, ok := any(text).(string); ok {
mode = s
} else {
mode = string(any(text).([]byte))
}
switch strings.ToLower(mode) {
case RotationModeDisabled.String(), "":
return RotationModeDisabled, nil // the empty string makes sense
case RotationModeRandomOnStartup.String():
return RotationModeRandomOnStartup, nil
case RotationModeRandomOnEachRequest.String():
return RotationModeRandomOnEachRequest, nil
case RotationModeRandomHourly.String():
return RotationModeRandomHourly, nil
case RotationModeRandomDaily.String():
return RotationModeRandomDaily, nil
}
return RotationModeDisabled, fmt.Errorf("unrecognized rotation mode: %q", mode)
}

View File

@ -0,0 +1,90 @@
package config_test
import (
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/config"
)
func TestRotationMode_String(t *testing.T) {
t.Parallel()
assert.Equal(t, "disabled", config.RotationModeDisabled.String())
assert.Equal(t, "random-on-startup", config.RotationModeRandomOnStartup.String())
assert.Equal(t, "random-on-each-request", config.RotationModeRandomOnEachRequest.String())
assert.Equal(t, "random-daily", config.RotationModeRandomDaily.String())
assert.Equal(t, "random-hourly", config.RotationModeRandomHourly.String())
assert.Equal(t, "RotationMode(255)", config.RotationMode(255).String())
}
func TestRotationModes(t *testing.T) {
t.Parallel()
assert.Equal(t, []config.RotationMode{
config.RotationModeDisabled,
config.RotationModeRandomOnStartup,
config.RotationModeRandomOnEachRequest,
config.RotationModeRandomHourly,
config.RotationModeRandomDaily,
}, config.RotationModes())
}
func TestRotationModeStrings(t *testing.T) {
t.Parallel()
assert.Equal(t, []string{
"disabled",
"random-on-startup",
"random-on-each-request",
"random-hourly",
"random-daily",
}, config.RotationModeStrings())
}
func TestParseRotationMode(t *testing.T) {
t.Parallel()
for name, _tt := range map[string]struct {
giveBytes []byte
giveString string
wantMode config.RotationMode
wantErrorMsg string
}{
"<empty string>": {giveString: "", wantMode: config.RotationModeDisabled},
"<empty bytes>": {giveBytes: []byte(""), wantMode: config.RotationModeDisabled},
"disabled": {giveString: "disabled", wantMode: config.RotationModeDisabled},
"disabled (bytes)": {giveBytes: []byte("disabled"), wantMode: config.RotationModeDisabled},
"random-on-startup": {giveString: "random-on-startup", wantMode: config.RotationModeRandomOnStartup},
"random-on-startup (bytes)": {giveBytes: []byte("random-on-startup"), wantMode: config.RotationModeRandomOnStartup},
"on-each-request": {giveString: "random-on-each-request", wantMode: config.RotationModeRandomOnEachRequest},
"daily": {giveString: "random-daily", wantMode: config.RotationModeRandomDaily},
"hourly": {giveString: "random-hourly", wantMode: config.RotationModeRandomHourly},
"foobar": {giveString: "foobar", wantErrorMsg: "unrecognized rotation mode: \"foobar\""},
} {
tt := _tt
t.Run(name, func(t *testing.T) {
var (
mode config.RotationMode
err error
)
if tt.giveString != "" || tt.giveBytes == nil {
mode, err = config.ParseRotationMode(tt.giveString)
} else {
mode, err = config.ParseRotationMode(tt.giveBytes)
}
if tt.wantErrorMsg == "" {
assert.NoError(t, err)
assert.Equal(t, tt.wantMode, mode)
} else {
assert.ErrorContains(t, err, tt.wantErrorMsg)
}
})
}
}

View File

@ -0,0 +1,105 @@
package config
import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
)
type templates map[string]string // map[name]content
// Add adds a new template.
func (tpl templates) Add(name, content string) error {
if name == "" {
return fmt.Errorf("template name cannot be empty")
}
tpl[name] = content
return nil
}
// AddFromFile reads the file content and adds it as a new template.
func (tpl templates) AddFromFile(path string, name ...string) (addedTemplateName string, _ error) {
// check if the file exists and is not a directory
if stat, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("file %s not found", path)
}
return "", err
} else if stat.IsDir() {
return "", fmt.Errorf("%s is not a file", path)
}
// read the file content
var content, err = os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("cannot read file %s: %w", path, err)
}
var templateName string
if len(name) > 0 && name[0] != "" { // if the name is provided, use it
templateName = name[0]
} else { // otherwise, use the file name without the extension
var (
fileName = filepath.Base(path)
ext = filepath.Ext(fileName)
)
if ext != "" && fileName != ext {
templateName = strings.TrimSuffix(fileName, ext)
} else {
templateName = fileName
}
}
// add the template to the config
tpl[templateName] = string(content)
return templateName, nil
}
// Names returns all template names sorted alphabetically.
func (tpl templates) Names() []string {
var names = make([]string, 0, len(tpl))
for name := range tpl {
names = append(names, name)
}
slices.Sort(names)
return names
}
// Has checks if the template with the specified name exists.
func (tpl templates) Has(name string) (found bool) { _, found = tpl[name]; return } //nolint:nlreturn
// Get returns the template content by the specified name, if it exists.
func (tpl templates) Get(name string) (data string, ok bool) { data, ok = tpl[name]; return } //nolint:nlreturn
// Remove deletes the template by the specified name.
func (tpl templates) Remove(name string) (ok bool) {
if _, ok = tpl[name]; ok {
delete(tpl, name)
}
return
}
// RandomName picks a random template name. It returns an empty string if there are no templates.
func (tpl templates) RandomName() string {
if len(tpl) == 0 {
return ""
}
for name := range tpl { // map iteration order is unpredictable (random) by design
return name
}
return ""
}

View File

@ -0,0 +1,164 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTemplates_Common(t *testing.T) {
t.Parallel()
var tpl = make(templates)
t.Run("initial state", func(t *testing.T) {
assert.Empty(t, tpl.Names())
assert.False(t, tpl.Has("test"))
var got, ok = tpl.Get("test")
assert.Empty(t, got)
assert.False(t, ok)
})
t.Run("add a template from variable", func(t *testing.T) {
const testContent = "content"
assert.NoError(t, tpl.Add("test", testContent))
assert.True(t, tpl.Has("test"))
var got, ok = tpl.Get("test")
assert.Equal(t, got, testContent)
assert.True(t, ok)
assert.Equal(t, []string{"test"}, tpl.Names())
assert.False(t, tpl.Has("_test99"))
assert.NoError(t, tpl.Add("_test99", ""))
assert.NoError(t, tpl.Add("_test11", ""))
assert.Equal(t, []string{"_test11", "_test99", "test"}, tpl.Names()) // sorted
assert.True(t, tpl.Has("_test99"))
assert.True(t, tpl.Remove("_test99"))
assert.False(t, tpl.Has("_test99"))
assert.False(t, tpl.Remove("_test99"))
})
t.Run("adding template without a name should fail", func(t *testing.T) {
assert.ErrorContains(t, tpl.Add("", "content"), "template name cannot be empty")
})
}
func TestTemplates_AddFromFile(t *testing.T) {
t.Parallel()
for name, _tt := range map[string]struct {
givePath string
giveName func() []string
wantError string
wantThisName string
wantThisContent string
}{
"dotfile": {
givePath: "./testdata/.dotfile",
wantThisName: ".dotfile",
},
"dotfile with extension": {
givePath: "./testdata/.dotfile_with.ext",
wantThisName: ".dotfile_with",
},
"empty file": {
givePath: "./testdata/empty.html",
wantThisName: "empty",
},
"file with multiple dots but without a name": {
givePath: "./testdata/file.with.multiple.dots",
wantThisName: "file.with.multiple",
},
"name with spaces": {
givePath: "./testdata/name with spaces.txt",
wantThisName: "name with spaces",
},
"with content and a name": {
givePath: "./testdata/with-content.htm",
giveName: func() []string { return []string{"test name"} },
wantThisName: "test name",
wantThisContent: "<!DOCTYPE html><html lang=\"en\"></html>\n",
},
"with content but without a name": {
givePath: "./testdata/with-content.htm",
wantThisName: "with-content",
wantThisContent: "<!DOCTYPE html><html lang=\"en\"></html>\n",
},
"filename with no extension": {
givePath: "./testdata/without_extension",
wantThisName: "without_extension",
},
"file not found": {
givePath: "./testdata/not-found",
wantError: "file ./testdata/not-found not found",
},
"directory": {
givePath: "./testdata",
wantError: "./testdata is not a file",
},
} {
var tt = _tt
t.Run(name, func(t *testing.T) {
t.Parallel()
var (
tpl = make(templates)
giveName []string
)
if tt.giveName != nil {
giveName = tt.giveName()
}
var addedName, err = tpl.AddFromFile(tt.givePath, giveName...)
if tt.wantError == "" {
assert.NoError(t, err)
assert.True(t, tpl.Has(tt.wantThisName))
assert.Equal(t, addedName, tt.wantThisName)
var content, _ = tpl.Get(tt.wantThisName)
assert.Equal(t, content, tt.wantThisContent)
} else {
assert.ErrorContains(t, err, tt.wantError)
assert.False(t, tpl.Has(tt.wantThisName))
}
})
}
}
func TestTemplates_RandomName(t *testing.T) {
t.Parallel()
var (
tpl = templates{"test": "content", "test2": "content", "test3": "content"}
lastName = tpl.RandomName()
changedCount int
)
for range 1_000 {
var name = tpl.RandomName()
if name != lastName {
changedCount++
}
lastName = name
}
// I expect at least 100 different names in 1000 iterations
assert.True(t, changedCount > 200)
}

0
internal/config/testdata/.dotfile vendored Normal file
View File

View File

View File

@ -1 +0,0 @@
<html><body>bar {{ code }}</body></html>

View File

@ -1 +0,0 @@
foo bar

0
internal/config/testdata/empty.html vendored Normal file
View File

View File

View File

@ -1 +0,0 @@
<html><body>foo {{ code }}</body></html>

View File

View File

@ -1,13 +0,0 @@
templates:
- path: ./foo-tpl.html
name: ghost # name is optional
- path: ./bar-tpl.html
pages:
400:
message: Bad Request
description: The server did not understand the request
401:
message: Unauthorized
description: The requested page needs a username and a password

View File

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"></html>

View File

26
internal/env/env.go vendored
View File

@ -1,26 +0,0 @@
// Package env contains all about environment variables, that can be used by current application.
package env
import "os"
type envVariable string
const (
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
ListenPort envVariable = "LISTEN_PORT" // port number for listening
TemplateName envVariable = "TEMPLATE_NAME" // template name
ConfigFilePath envVariable = "CONFIG_FILE" // path to the config file
DefaultErrorPage envVariable = "DEFAULT_ERROR_PAGE" // default error page (code)
DefaultHTTPCode envVariable = "DEFAULT_HTTP_CODE" // default HTTP response code
ShowDetails envVariable = "SHOW_DETAILS" // show request details in response
ProxyHTTPHeaders envVariable = "PROXY_HTTP_HEADERS" // proxy HTTP request headers list (request -> response)
DisableL10n envVariable = "DISABLE_L10N" // disable pages localization
)
// String returns environment variable name in the string representation.
func (e envVariable) String() string { return string(e) }
// Lookup retrieves the value of the environment variable. If the variable is present in the environment the value
// (which may be empty) is returned and the boolean is true. Otherwise the returned value will be empty and the
// boolean will be false.
func (e envVariable) Lookup() (string, bool) { return os.LookupEnv(string(e)) }

View File

@ -1,55 +0,0 @@
package env
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConstants(t *testing.T) {
assert.Equal(t, "LISTEN_ADDR", string(ListenAddr))
assert.Equal(t, "LISTEN_PORT", string(ListenPort))
assert.Equal(t, "TEMPLATE_NAME", string(TemplateName))
assert.Equal(t, "CONFIG_FILE", string(ConfigFilePath))
assert.Equal(t, "DEFAULT_ERROR_PAGE", string(DefaultErrorPage))
assert.Equal(t, "DEFAULT_HTTP_CODE", string(DefaultHTTPCode))
assert.Equal(t, "SHOW_DETAILS", string(ShowDetails))
assert.Equal(t, "PROXY_HTTP_HEADERS", string(ProxyHTTPHeaders))
assert.Equal(t, "DISABLE_L10N", string(DisableL10n))
}
func TestEnvVariable_Lookup(t *testing.T) {
cases := []struct {
giveEnv envVariable
}{
{giveEnv: ListenAddr},
{giveEnv: ListenPort},
{giveEnv: TemplateName},
{giveEnv: ConfigFilePath},
{giveEnv: DefaultErrorPage},
{giveEnv: DefaultHTTPCode},
{giveEnv: ShowDetails},
{giveEnv: ProxyHTTPHeaders},
{giveEnv: DisableL10n},
}
for _, tt := range cases {
tt := tt
t.Run(tt.giveEnv.String(), func(t *testing.T) {
assert.NoError(t, os.Unsetenv(tt.giveEnv.String())) // make sure that env is unset for test
defer func() { assert.NoError(t, os.Unsetenv(tt.giveEnv.String())) }()
value, exists := tt.giveEnv.Lookup()
assert.False(t, exists)
assert.Empty(t, value)
assert.NoError(t, os.Setenv(tt.giveEnv.String(), "foo"))
value, exists = tt.giveEnv.Lookup()
assert.True(t, exists)
assert.Equal(t, "foo", value)
})
}
}

View File

@ -1,68 +0,0 @@
package common
import (
"strings"
"time"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler {
const headersSeparator = ": "
return func(ctx *fasthttp.RequestCtx) {
var ua = string(ctx.UserAgent())
if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging
h(ctx)
return
}
var reqHeaders = make([]string, 0, 24) //nolint:gomnd
ctx.Request.Header.VisitAll(func(key, value []byte) {
reqHeaders = append(reqHeaders, string(key)+headersSeparator+string(value))
})
var startedAt = time.Now()
h(ctx)
var respHeaders = make([]string, 0, 16) //nolint:gomnd
ctx.Response.Header.VisitAll(func(key, value []byte) {
respHeaders = append(respHeaders, string(key)+headersSeparator+string(value))
})
log.Info("HTTP request processed",
zap.String("useragent", ua),
zap.String("method", string(ctx.Method())),
zap.String("url", string(ctx.RequestURI())),
zap.String("referer", string(ctx.Referer())),
zap.Int("status_code", ctx.Response.StatusCode()),
zap.String("content_type", string(ctx.Response.Header.ContentType())),
zap.Bool("connection_close", ctx.Response.ConnectionClose()),
zap.Duration("duration", time.Since(startedAt)),
zap.Strings("request_headers", reqHeaders),
zap.Strings("response_headers", respHeaders),
)
}
}
type metrics interface {
IncrementTotalRequests()
ObserveRequestDuration(t time.Duration)
}
func DurationMetrics(h fasthttp.RequestHandler, m metrics) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
var startedAt = time.Now()
h(ctx)
m.IncrementTotalRequests()
m.ObserveRequestDuration(time.Since(startedAt))
}
}

View File

@ -1,7 +0,0 @@
package common_test
import "testing"
func TestNothing2(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,126 +0,0 @@
package core
import (
"strconv"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/options"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/valyala/fasthttp"
)
type templatePicker interface {
// Pick the template name for responding.
Pick() string
}
type renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
func RespondWithErrorPage( //nolint:funlen,gocyclo
ctx *fasthttp.RequestCtx,
cfg *config.Config,
p templatePicker,
rdr renderer,
pageCode string,
httpCode int,
opt options.ErrorPage,
) {
ctx.Response.Header.Set("X-Robots-Tag", "noindex") // block Search indexing
var (
clientWant = ClientWantFormat(ctx)
json, canJSON = cfg.JSONFormat()
xml, canXML = cfg.XMLFormat()
props = tpl.Properties{
Code: pageCode,
ShowRequestDetails: opt.ShowDetails,
L10nDisabled: opt.L10n.Disabled,
}
)
if opt.ShowDetails {
props.OriginalURI = string(ctx.Request.Header.Peek(OriginalURI))
props.Namespace = string(ctx.Request.Header.Peek(Namespace))
props.IngressName = string(ctx.Request.Header.Peek(IngressName))
props.ServiceName = string(ctx.Request.Header.Peek(ServiceName))
props.ServicePort = string(ctx.Request.Header.Peek(ServicePort))
props.RequestID = string(ctx.Request.Header.Peek(RequestID))
props.ForwardedFor = string(ctx.Request.Header.Peek(ForwardedFor))
props.Host = string(ctx.Request.Header.Peek(Host))
}
if page, exists := cfg.Pages[pageCode]; exists {
props.Message = page.Message()
props.Description = page.Description()
} else if c, err := strconv.Atoi(pageCode); err == nil {
if s := fasthttp.StatusMessage(c); s != "Unknown Status Code" { // as a fallback
props.Message = s
}
}
SetClientFormat(ctx, PlainTextContentType) // set default content type
if props.Message == "" {
ctx.SetStatusCode(fasthttp.StatusNotFound)
_, _ = ctx.WriteString("requested pageCode (" + pageCode + ") not available")
return
}
// proxy required HTTP headers from the request to the response
for _, headerToProxy := range opt.ProxyHTTPHeaders {
if reqHeader := ctx.Request.Header.Peek(headerToProxy); len(reqHeader) > 0 {
ctx.Response.Header.SetBytesV(headerToProxy, reqHeader)
}
}
switch {
case clientWant == JSONContentType && canJSON: // JSON
{
SetClientFormat(ctx, JSONContentType)
if content, err := rdr.Render(json.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render JSON template: " + err.Error())
}
}
case clientWant == XMLContentType && canXML: // XML
{
SetClientFormat(ctx, XMLContentType)
if content, err := rdr.Render(xml.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render XML template: " + err.Error())
}
}
default: // HTML
{
SetClientFormat(ctx, HTMLContentType)
var templateName = p.Pick()
if template, exists := cfg.Template(templateName); exists {
if content, err := rdr.Render(template.Content(), props); err == nil {
ctx.SetStatusCode(httpCode)
_, _ = ctx.Write(content)
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot render HTML template: " + err.Error())
}
} else {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("template " + templateName + " not exists")
}
}
}
}

View File

@ -1,102 +0,0 @@
package core
import (
"bytes"
"sort"
"strconv"
"github.com/valyala/fasthttp"
)
type ContentType = byte
const (
UnknownContentType ContentType = iota // should be first
JSONContentType
XMLContentType
HTMLContentType
PlainTextContentType
)
func ClientWantFormat(ctx *fasthttp.RequestCtx) ContentType {
// parse "Content-Type" header (e.g.: `application/json;charset=UTF-8`)
if ct := bytes.ToLower(ctx.Request.Header.ContentType()); len(ct) > 4 { //nolint:gomnd
return mimeTypeToContentType(ct)
}
// parse `X-Format` header (aka `Accept`) for the Ingress support
// e.g.: `text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8`
if h := bytes.ToLower(bytes.TrimSpace(ctx.Request.Header.Peek(FormatHeader))); len(h) > 2 { //nolint:gomnd,nestif
type format struct {
mimeType []byte
weight float32
}
var formats = make([]format, 0, 8) //nolint:gomnd
for _, b := range bytes.FieldsFunc(h, func(r rune) bool { return r == ',' }) {
if idx := bytes.Index(b, []byte(";q=")); idx > 0 && idx < len(b) {
f := format{b[0:idx], 0}
if len(b) > idx+3 {
if weight, err := strconv.ParseFloat(string(b[idx+3:]), 32); err == nil { //nolint:gomnd
f.weight = float32(weight)
}
}
formats = append(formats, f)
} else {
formats = append(formats, format{b, 1})
}
}
switch l := len(formats); {
case l == 0:
return UnknownContentType
case l == 1:
return mimeTypeToContentType(formats[0].mimeType)
default:
sort.SliceStable(formats, func(i, j int) bool { return formats[i].weight > formats[j].weight })
return mimeTypeToContentType(formats[0].mimeType)
}
}
return UnknownContentType
}
func mimeTypeToContentType(mimeType []byte) ContentType {
switch {
case bytes.Contains(mimeType, []byte("application/json")), bytes.Contains(mimeType, []byte("text/json")):
return JSONContentType
case bytes.Contains(mimeType, []byte("application/xml")), bytes.Contains(mimeType, []byte("text/xml")):
return XMLContentType
case bytes.Contains(mimeType, []byte("text/html")):
return HTMLContentType
case bytes.Contains(mimeType, []byte("text/plain")):
return PlainTextContentType
}
return UnknownContentType
}
func SetClientFormat(ctx *fasthttp.RequestCtx, t ContentType) {
switch t {
case JSONContentType:
ctx.SetContentType("application/json; charset=utf-8")
case XMLContentType:
ctx.SetContentType("application/xml; charset=utf-8")
case HTMLContentType:
ctx.SetContentType("text/html; charset=utf-8")
case PlainTextContentType:
ctx.SetContentType("text/plain; charset=utf-8")
}
}

View File

@ -1,117 +0,0 @@
package core_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/http/core"
"github.com/valyala/fasthttp"
)
func TestClientWantFormat(t *testing.T) {
for name, tt := range map[string]struct {
giveContentTypeHeader string
giveFormatHeader string
giveReqCtx func() *fasthttp.RequestCtx
wantFormat core.ContentType
}{
"priority": {
giveFormatHeader: "application/xml",
giveContentTypeHeader: "text/plain",
wantFormat: core.PlainTextContentType,
},
"format respects weight": {
giveFormatHeader: "text/html;q=0.5,application/xhtml+xml;q=0.9,application/xml;q=1,*/*;q=0.8",
wantFormat: core.XMLContentType,
},
"wrong format value": {
giveFormatHeader: ";q=foobar,bar/baz;;;;;application/xml",
wantFormat: core.UnknownContentType,
},
"content type - application/json": {
giveContentTypeHeader: "application/jsoN; charset=utf-8", wantFormat: core.JSONContentType,
},
"content type - text/json": {
giveContentTypeHeader: "text/Json; charset=utf-8", wantFormat: core.JSONContentType,
},
"format - json": {
giveFormatHeader: "application/jsoN,*/*;q=0.8", wantFormat: core.JSONContentType,
},
"content type - application/xml": {
giveContentTypeHeader: "application/xmL; charset=utf-8", wantFormat: core.XMLContentType,
},
"content type - text/xml": {
giveContentTypeHeader: "text/Xml; charset=utf-8", wantFormat: core.XMLContentType,
},
"format - xml": {
giveFormatHeader: "text/Xml", wantFormat: core.XMLContentType,
},
"content type - text/html": {
giveContentTypeHeader: "text/htMl; charset=utf-8", wantFormat: core.HTMLContentType,
},
"format - html": {
giveFormatHeader: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
wantFormat: core.HTMLContentType,
},
"content type - text/plain": {
giveContentTypeHeader: "text/plaiN; charset=utf-8", wantFormat: core.PlainTextContentType,
},
"format - plain": {
giveFormatHeader: "text/plaiN,text/html,application/xml;q=0.9,,,*/*;q=0.8", wantFormat: core.PlainTextContentType,
},
"unknown on empty": {
wantFormat: core.UnknownContentType,
},
"unknown on foo/bar": {
giveContentTypeHeader: "foo/bar; charset=utf-8",
giveFormatHeader: "foo/bar; charset=utf-8",
wantFormat: core.UnknownContentType,
},
} {
t.Run(name, func(t *testing.T) {
h := &fasthttp.RequestHeader{}
h.Set(fasthttp.HeaderContentType, tt.giveContentTypeHeader)
h.Set(core.FormatHeader, tt.giveFormatHeader)
ctx := &fasthttp.RequestCtx{
Request: fasthttp.Request{
Header: *h, //nolint:govet
},
}
assert.Equal(t, tt.wantFormat, core.ClientWantFormat(ctx))
})
}
}
func TestSetClientFormat(t *testing.T) {
for name, tt := range map[string]struct {
giveContentType core.ContentType
wantHeaderValue string
}{
"plain on unknown": {giveContentType: core.UnknownContentType, wantHeaderValue: "text/plain; charset=utf-8"},
"json": {giveContentType: core.JSONContentType, wantHeaderValue: "application/json; charset=utf-8"},
"xml": {giveContentType: core.XMLContentType, wantHeaderValue: "application/xml; charset=utf-8"},
"html": {giveContentType: core.HTMLContentType, wantHeaderValue: "text/html; charset=utf-8"},
"plain": {giveContentType: core.PlainTextContentType, wantHeaderValue: "text/plain; charset=utf-8"},
} {
t.Run(name, func(t *testing.T) {
ctx := &fasthttp.RequestCtx{
Response: fasthttp.Response{
Header: fasthttp.ResponseHeader{},
},
}
assert.Empty(t, "", ctx.Response.Header.Peek(fasthttp.HeaderContentType))
core.SetClientFormat(ctx, tt.giveContentType)
assert.Equal(t, tt.wantHeaderValue, string(ctx.Response.Header.Peek(fasthttp.HeaderContentType)))
})
}
}

View File

@ -1,33 +0,0 @@
package core
const (
// FormatHeader name of the header used to extract the format
FormatHeader = "X-Format"
// CodeHeader name of the header used as source of the HTTP status code to return
CodeHeader = "X-Code"
// OriginalURI name of the header with the original URL from NGINX
OriginalURI = "X-Original-URI"
// Namespace name of the header that contains information about the Ingress namespace
Namespace = "X-Namespace"
// IngressName name of the header that contains the matched Ingress
IngressName = "X-Ingress-Name"
// ServiceName name of the header that contains the matched Service in the Ingress
ServiceName = "X-Service-Name"
// ServicePort name of the header that contains the matched Service port in the Ingress
ServicePort = "X-Service-Port"
// RequestID is a unique ID that identifies the request - same as for backend service
RequestID = "X-Request-ID"
// ForwardedFor identifies the user of this session
ForwardedFor = "X-Forwarded-For"
// Host identifies the hosts origin
Host = "Host"
)

View File

@ -0,0 +1,62 @@
package error_page
import (
"path/filepath"
"strconv"
"strings"
"github.com/valyala/fasthttp"
)
// extractCodeFromURL extracts the error code from the given URL.
func extractCodeFromURL(url string) (uint16, bool) {
var parts = strings.SplitN(strings.TrimLeft(url, "/"), "/", 1)
if len(parts) == 0 {
return 0, false
}
var (
fileName = strings.ToLower(parts[0])
ext = filepath.Ext(fileName) // ".html", ".htm", ".%something%" or an empty string
)
if ext != "" && ext != ".html" && ext != ".htm" {
return 0, false
} else if ext != "" {
fileName = strings.TrimSuffix(fileName, ext)
}
if code, err := strconv.ParseUint(fileName, 10, 16); err == nil && code > 0 && code < 999 {
return uint16(code), true
}
return 0, false
}
// URLContainsCode checks if the given URL contains an error code.
func URLContainsCode(url string) (ok bool) { _, ok = extractCodeFromURL(url); return } //nolint:nlreturn
// extractCodeFromHeaders extracts the error code from the given headers.
func extractCodeFromHeaders(headers *fasthttp.RequestHeader) (uint16, bool) {
if headers == nil {
return 0, false
}
// https://kubernetes.github.io/ingress-nginx/user-guide/custom-errors/
// HTTP status code returned by the request
if value := headers.Peek("X-Code"); len(value) > 0 && len(value) <= 3 {
if code, err := strconv.ParseUint(string(value), 10, 16); err == nil && code > 0 && code < 999 {
return uint16(code), true
}
}
return 0, false
}
// HeadersContainCode checks if the given headers contain an error code.
func HeadersContainCode(headers *fasthttp.RequestHeader) (ok bool) {
_, ok = extractCodeFromHeaders(headers)
return
}

View File

@ -0,0 +1,66 @@
package error_page_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
"gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
)
func TestURLContainsCode(t *testing.T) {
t.Parallel()
for giveUrl, wantOk := range map[string]bool{
"/404": true,
"/404.htm": true,
"/404.HTM": true,
"/404.html": true,
"/404.HtmL": true,
"/404.css": false,
"/foo/404": false,
"/foo/404.html": false,
"/error": false,
"/": false,
"/////": false,
"///404//": false,
"": false,
} {
t.Run(giveUrl, func(t *testing.T) {
assert.Equal(t, wantOk, error_page.URLContainsCode(giveUrl))
})
}
}
func TestHeadersContainCode(t *testing.T) {
t.Parallel()
var mkHeaders = func(key, value string) *fasthttp.RequestHeader {
var out = new(fasthttp.RequestHeader)
out.Set(key, value)
return out
}
for name, _tt := range map[string]struct {
giveHeaders *fasthttp.RequestHeader
wantOk bool
}{
"with code": {giveHeaders: mkHeaders("X-Code", "404"), wantOk: true},
"empty": {giveHeaders: nil},
"no code": {giveHeaders: mkHeaders("X-Code", "")},
"wrong": {giveHeaders: mkHeaders("X-Code", "foo")},
"too big": {giveHeaders: mkHeaders("X-Code", "1000")},
"too small": {giveHeaders: mkHeaders("X-Code", "0")},
"negative": {giveHeaders: mkHeaders("X-Code", "-1")},
} {
tt := _tt
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.wantOk, error_page.HeadersContainCode(tt.giveHeaders))
})
}
}

View File

@ -0,0 +1,135 @@
package error_page
import (
"math"
"slices"
"strconv"
"strings"
"github.com/valyala/fasthttp"
)
type preferredFormat = byte
const (
unknownFormat preferredFormat = iota // should be first, no format detected
jsonFormat // json
xmlFormat // xml
htmlFormat // html
plainTextFormat // plain text
)
// detectPreferredFormatForClient detects the preferred format for the client based on the headers.
// It supports the following headers: Content-Type, Accept, X-Format.
// If the headers are not set or the format is not recognized, it returns unknownFormat.
func detectPreferredFormatForClient(headers *fasthttp.RequestHeader) preferredFormat { //nolint:funlen,gocognit
var contentType, accept string
if contentTypeHeader := strings.TrimSpace(string(headers.Peek("Content-Type"))); contentTypeHeader != "" { //nolint:nestif,lll
// https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Type
// text/html; charset=utf-8
// multipart/form-data; boundary=something
// application/json
if parts := strings.SplitN(contentTypeHeader, ";", 2); len(parts) > 1 { //nolint:mnd
// take only the first part of the content type:
// text/html; charset=utf-8
// ^^^^^^^^^ - will be taken
contentType = strings.TrimSpace(parts[0])
} else {
// take the whole value
contentType = contentTypeHeader
}
} else if xFormatHeader := strings.TrimSpace(string(headers.Peek("X-Format"))); xFormatHeader != "" {
// https://kubernetes.github.io/ingress-nginx/user-guide/custom-errors/
// Value of the `Accept` header sent by the client
accept = xFormatHeader
} else if acceptHeader := strings.TrimSpace(string(headers.Peek("Accept"))); acceptHeader != "" {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
// text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8
// text/html
// image/*
// */*
// text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept = acceptHeader
} else {
return unknownFormat
}
switch {
case contentType != "":
return mimeTypeToPreferredFormat(contentType)
case accept != "":
type piece struct {
mimeType string
weight int // to avoid float32 comparison (weight 1.0 = 1_0, 0.9 = 0_9, 0.8 = 0_8, etc.)
}
var pieces = make([]piece, 0, strings.Count(accept, ",")+1)
// split application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 into parts:
// ^^^^^^^^^ - segment #3
// ^^^^^^^^^^^^^^^^^^^^^ - segment #2
// ^^^^^^^^^^^^^^^^^^^^^ - segment #1
for _, segment := range strings.FieldsFunc(accept, func(r rune) bool { return r == ',' }) {
// split segment into parts:
//
// application/xhtml+xml
// ^^^^^^^^^^^^^^^^^^^^^ - part #1
//
// application/xml;q=0.9
// ^^^^^ - part #2
// ^^^^^^^^^^^^^^^ - part #1
//
// */*;q=0.8
// ^^^^^ - part #2
// ^^^ - part #1
if parts := strings.SplitN(strings.TrimSpace(segment), ";", 2); len(parts) > 0 { //nolint:mnd,nestif
if parts[0] == "*/*" {
continue // skip the wildcard
}
var p = piece{mimeType: parts[0], weight: 1_0} //nolint:mnd // by default the weight is 10 (1.0 in float)
if len(parts) > 1 { // we need to extract the weight
// trim the `q=` prefix and try to parse the weight value
if weight, err := strconv.ParseFloat(strings.TrimPrefix(strings.ToLower(parts[1]), "q="), 32); err == nil {
if weight = math.Round(weight*100) / 100; weight <= 1 && weight >= 0 { //nolint:mnd
p.weight = int(weight * 10) //nolint:mnd
} else {
p.weight = 0 // invalid weight, set it to 0
}
}
}
pieces = append(pieces, p)
}
}
if len(pieces) > 0 {
slices.SortStableFunc(pieces, func(a, b piece) int { return b.weight - a.weight })
return mimeTypeToPreferredFormat(pieces[0].mimeType)
}
}
return unknownFormat
}
// mimeTypeToPreferredFormat converts a MIME type to a preferred format, using non-string comparison.
func mimeTypeToPreferredFormat(mimeType string) preferredFormat {
switch value := strings.ToLower(mimeType); {
case strings.Contains(value, "/json"): // application/json text/json
return jsonFormat
case strings.Contains(value, "/xml"): // application/xml text/xml
return xmlFormat
case strings.Contains(value, "+xml"): // application/xhtml+xml
return xmlFormat
case strings.Contains(value, "/html"): // text/html
return htmlFormat
case strings.Contains(value, "/plain"): // text/plain
return plainTextFormat
}
return unknownFormat
}

View File

@ -0,0 +1,114 @@
package error_page
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp"
)
func Test_detectPreferredFormatForClient(t *testing.T) {
t.Parallel()
for name, _tt := range map[string]struct {
giveHeaders map[string][]string
wantFormat preferredFormat
}{
"content type json": {
giveHeaders: map[string][]string{"Content-Type": {"application/jSoN"}},
wantFormat: jsonFormat,
},
"content type xml": {
giveHeaders: map[string][]string{"Content-Type": {"application/xml; charset=UTF-8"}},
wantFormat: xmlFormat,
},
"content type html": {
giveHeaders: map[string][]string{"Content-Type": {"text/hTmL; charset=utf-8"}},
wantFormat: htmlFormat,
},
"content type plain": {
giveHeaders: map[string][]string{"Content-Type": {"text/plaIN"}},
wantFormat: plainTextFormat,
},
"accept json": {
giveHeaders: map[string][]string{"Accept": {"application/jsoN,*/*;q=0.8"}},
wantFormat: jsonFormat,
},
"accept xml, depends on weight": {
giveHeaders: map[string][]string{"Accept": {"text/html;q=0.5,application/xhtml+xml;q=0.9,application/xml;q=1,*/*;q=0.8"}},
wantFormat: xmlFormat,
},
"accept json, depends on weight": {
giveHeaders: map[string][]string{"Accept": {"application/jsoN,*/*;q=0.8"}},
wantFormat: jsonFormat,
},
"accept xml": {
giveHeaders: map[string][]string{"Accept": {"application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}},
wantFormat: xmlFormat,
},
"accept html": {
giveHeaders: map[string][]string{"Accept": {"text/html, application/xhtml+xml, application/xml;q=0.9, image/avif, image/webp, */*;q=0.8"}},
wantFormat: htmlFormat,
},
"accept plain": {
giveHeaders: map[string][]string{"Accept": {"text/plaiN,text/html,application/xml;q=0.9,,,*/*;q=0.8"}},
wantFormat: plainTextFormat,
},
"accept json, weighted values only": {
giveHeaders: map[string][]string{"Accept": {"application/jsoN;Q=0.1,text/html;q=1.1,application/xml;q=-1,*/*;q=0.8"}},
wantFormat: jsonFormat,
},
"x-format json, depends on weight": {
giveHeaders: map[string][]string{"X-Format": {"application/jsoN,*/*;q=0.8"}},
wantFormat: jsonFormat,
},
"x-format xml": {
giveHeaders: map[string][]string{"X-Format": {"application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}},
wantFormat: xmlFormat,
},
"content type has priority over accept": {
giveHeaders: map[string][]string{"Content-Type": {"text/plain"}, "Accept": {"application/xml"}},
wantFormat: plainTextFormat,
},
"accept has priority over x-format": {
giveHeaders: map[string][]string{"Accept": {"application/xml"}, "X-Format": {"text/plain"}},
wantFormat: plainTextFormat,
},
"empty headers": {
giveHeaders: nil,
},
"empty content type": {
giveHeaders: map[string][]string{"Content-Type": {" "}},
},
"wrong content type": {
giveHeaders: map[string][]string{"Content-Type": {"multipart/form-data; boundary=something"}},
},
"wrong accept": {
giveHeaders: map[string][]string{"Accept": {";q=foobar,bar/baz;;;;;application/xml"}},
},
"none on invalid input": {
giveHeaders: map[string][]string{"Content-Type": {"foo/bar; charset=utf-8"}, "Accept": {"foo/bar; charset=utf-8"}},
},
"completely unknown": {
giveHeaders: map[string][]string{"Content-Type": {"😀"}, "Accept": {"😄"}, "X-Format": {"😍"}},
},
} {
tt := _tt
t.Run(name, func(t *testing.T) {
var headers = new(fasthttp.RequestHeader)
for key, values := range tt.giveHeaders {
for _, value := range values {
headers.Add(key, value)
}
}
assert.Equal(t, tt.wantFormat, detectPreferredFormatForClient(headers))
})
}
}

View File

@ -0,0 +1,225 @@
package error_page
import (
"encoding/json"
"fmt"
"net/http"
"sync/atomic"
"time"
"github.com/valyala/fasthttp"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/logger"
"gh.tarampamp.am/error-pages/internal/template"
)
// New creates a new handler that returns an error page with the specified status code and format.
func New(cfg *config.Config, log *logger.Logger) fasthttp.RequestHandler { //nolint:funlen,gocognit,gocyclo
return func(ctx *fasthttp.RequestCtx) {
var (
reqHeaders = &ctx.Request.Header
code uint16
)
if fromUrl, okUrl := extractCodeFromURL(string(ctx.Path())); okUrl {
code = fromUrl
} else if fromHeader, okHeaders := extractCodeFromHeaders(reqHeaders); okHeaders {
code = fromHeader
} else {
code = cfg.DefaultCodeToRender
}
var httpCode int
if cfg.RespondWithSameHTTPCode {
httpCode = int(code)
} else {
httpCode = http.StatusOK
}
var format = detectPreferredFormatForClient(reqHeaders)
{ // deal with the headers
switch format {
case jsonFormat:
ctx.SetContentType("application/json; charset=utf-8")
case xmlFormat:
ctx.SetContentType("application/xml; charset=utf-8")
case htmlFormat:
ctx.SetContentType("text/html; charset=utf-8")
default:
ctx.SetContentType("text/plain; charset=utf-8") // plainTextFormat as default
}
// https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
// disallow indexing of the error pages
ctx.Response.Header.Set("X-Robots-Tag", "noindex")
switch code {
case http.StatusRequestTimeout, http.StatusTooEarly, http.StatusTooManyRequests,
http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable,
http.StatusGatewayTimeout:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
// tell the client (search crawler) to retry the request after 120 seconds
ctx.Response.Header.Set("Retry-After", "120")
}
// proxy the headers from the incoming request to the error page response if they are defined in the config
for _, proxyHeader := range cfg.ProxyHeaders {
if value := reqHeaders.Peek(proxyHeader); len(value) > 0 {
ctx.Response.Header.SetBytesV(proxyHeader, value)
}
}
}
ctx.SetStatusCode(httpCode)
// prepare the template properties for rendering
var tplProps = template.Props{
Code: code, // http status code
ShowRequestDetails: cfg.ShowDetails, // status message
L10nDisabled: cfg.L10n.Disable, // status description
}
//nolint:lll
if cfg.ShowDetails { // https://kubernetes.github.io/ingress-nginx/user-guide/custom-errors/
tplProps.OriginalURI = string(reqHeaders.Peek("X-Original-URI")) // (ingress-nginx) URI that caused the error
tplProps.Namespace = string(reqHeaders.Peek("X-Namespace")) // (ingress-nginx) namespace where the backend Service is located
tplProps.IngressName = string(reqHeaders.Peek("X-Ingress-Name")) // (ingress-nginx) name of the Ingress where the backend is defined
tplProps.ServiceName = string(reqHeaders.Peek("X-Service-Name")) // (ingress-nginx) name of the Service backing the backend
tplProps.ServicePort = string(reqHeaders.Peek("X-Service-Port")) // (ingress-nginx) port number of the Service backing the backend
tplProps.RequestID = string(reqHeaders.Peek("X-Request-Id")) // (ingress-nginx) unique ID that identifies the request - same as for backend service
tplProps.ForwardedFor = string(reqHeaders.Peek("X-Forwarded-For")) // the value of the `X-Forwarded-For` header
tplProps.Host = string(reqHeaders.Peek("Host")) // the value of the `Host` header
}
// try to find the code message and description in the config and if not - use the standard status text or fallback
if desc, found := cfg.Codes.Find(code); found {
tplProps.Message = desc.Message
tplProps.Description = desc.Description
} else if stdlibStatusText := http.StatusText(int(code)); stdlibStatusText != "" {
tplProps.Message = stdlibStatusText
} else {
tplProps.Message = "Unknown Status Code" // fallback
}
switch {
case format == jsonFormat && cfg.Formats.JSON != "":
if content, err := template.Render(cfg.Formats.JSON, tplProps); err != nil {
j, _ := json.Marshal(fmt.Sprintf("Failed to render the JSON template: %s", err.Error()))
write(ctx, log, j)
} else {
write(ctx, log, content)
}
case format == xmlFormat && cfg.Formats.XML != "":
if content, err := template.Render(cfg.Formats.XML, tplProps); err != nil {
write(ctx, log, fmt.Sprintf(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<error>Failed to render the XML template: %s</error>", err.Error(),
))
} else {
write(ctx, log, content)
}
case format == htmlFormat:
var templateName = templateToUse(cfg)
if tpl, found := cfg.Templates.Get(templateName); found {
if content, err := template.Render(tpl, tplProps); err != nil {
// TODO: add GZIP compression for the HTML content support
write(ctx, log, fmt.Sprintf(
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>",
templateName,
err.Error(),
))
} else {
write(ctx, log, content)
}
} else {
write(ctx, log, fmt.Sprintf(
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>", templateName,
))
}
default: // plainTextFormat as default
if cfg.Formats.PlainText != "" {
if content, err := template.Render(cfg.Formats.PlainText, tplProps); err != nil {
write(ctx, log, fmt.Sprintf("Failed to render the PlainText template: %s", err.Error()))
} else {
write(ctx, log, content)
}
} else {
write(ctx, log, `The requested content format is not supported.
Please create an issue on the project's GitHub page to request support for this format.
Supported formats: JSON, XML, HTML, Plain Text`)
}
}
}
}
var (
templateChangedAt atomic.Pointer[time.Time] //nolint:gochecknoglobals // the time when the theme was changed last time
pickedTemplate atomic.Pointer[string] //nolint:gochecknoglobals // the name of the randomly picked template
)
// templateToUse decides which template to use based on the rotation mode and the last time the template was changed.
func templateToUse(cfg *config.Config) string {
switch rotationMode := cfg.RotationMode; rotationMode {
case config.RotationModeDisabled:
return cfg.TemplateName // not needed to do anything
case config.RotationModeRandomOnStartup:
return cfg.TemplateName // do nothing, the scope of this rotation mode is not here
case config.RotationModeRandomOnEachRequest:
return cfg.Templates.RandomName() // pick a random template on each request
case config.RotationModeRandomHourly, config.RotationModeRandomDaily:
var now, rndTemplate = time.Now(), cfg.Templates.RandomName()
if changedAt := templateChangedAt.Load(); changedAt == nil {
// the template was not changed yet (first request)
templateChangedAt.Store(&now)
pickedTemplate.Store(&rndTemplate)
return rndTemplate
} else {
// is it time to change the template?
if (rotationMode == config.RotationModeRandomHourly && changedAt.Hour() != now.Hour()) ||
(rotationMode == config.RotationModeRandomDaily && changedAt.Day() != now.Day()) {
templateChangedAt.Store(&now)
pickedTemplate.Store(&rndTemplate)
return rndTemplate
} else if lastUsed := pickedTemplate.Load(); lastUsed != nil {
// time to change the template has not come yet, so use the last picked template
return *lastUsed
} else {
// in case if the last picked template is not set, pick a random one and store it
templateChangedAt.Store(&now)
pickedTemplate.Store(&rndTemplate)
return rndTemplate
}
}
}
return cfg.TemplateName // the fallback of the fallback :D
}
// write the content to the response writer and log the error if any.
func write[T string | []byte](ctx *fasthttp.RequestCtx, log *logger.Logger, content T) {
var data []byte
if s, ok := any(content).(string); ok {
data = []byte(s)
} else {
data = any(content).([]byte)
}
if _, err := ctx.Write(data); err != nil && log != nil {
log.Error("failed to write the response body",
logger.String("content", string(data)),
logger.Error(err),
)
}
}

View File

@ -0,0 +1,223 @@
package error_page_test
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/http/handlers/error_page"
"gh.tarampamp.am/error-pages/internal/http/httptest"
"gh.tarampamp.am/error-pages/internal/logger"
)
func TestHandler(t *testing.T) {
t.Parallel()
for name, tt := range map[string]struct {
giveConfig func() *config.Config
giveUrl string
giveHeaders map[string]string
wantStatusCode int
wantHeaders map[string]string
wantBodyIncludes []string
}{
"common, plain text": {
giveConfig: func() *config.Config { cfg := config.New(); return &cfg },
giveUrl: "http://testing/",
giveHeaders: map[string]string{"Content-Type": "text/plain"},
wantStatusCode: http.StatusOK,
wantHeaders: map[string]string{"Content-Type": "text/plain; charset=utf-8"},
wantBodyIncludes: []string{"Error 404", "Not Found"},
},
"common, html": {
giveConfig: func() *config.Config {
cfg := config.New()
cfg.TemplateName = "ghost"
return &cfg
},
giveUrl: "http://testing/",
giveHeaders: map[string]string{"X-Format": "text/html", "X-Code": "407"},
wantStatusCode: http.StatusOK,
wantHeaders: map[string]string{"Content-Type": "text/html; charset=utf-8"},
wantBodyIncludes: []string{
"<!DOCTYPE html>",
"<title>407: Proxy Authentication Required",
"Proxy Authentication Required",
},
},
"common, json": {
giveConfig: func() *config.Config {
cfg := config.New()
cfg.RespondWithSameHTTPCode = true
return &cfg
},
giveUrl: "http://testing/503.html?rnd=123",
giveHeaders: map[string]string{"Accept": "application/json", "X-FooBar": "baz"},
wantStatusCode: http.StatusServiceUnavailable,
wantHeaders: map[string]string{
"Content-Type": "application/json; charset=utf-8",
"X-FooBar": "", // is not in the list of proxy headers
},
wantBodyIncludes: []string{"503", "Service Unavailable"},
},
"common, xml": {
giveConfig: func() *config.Config {
cfg := config.New()
cfg.ProxyHeaders = append(cfg.ProxyHeaders, "X-FooBar")
return &cfg
},
giveUrl: "http://testing/500",
giveHeaders: map[string]string{"Accept": "application/xml", "X-FooBar": "baz"},
wantStatusCode: http.StatusOK,
wantHeaders: map[string]string{
"Content-Type": "application/xml; charset=utf-8",
"X-FooBar": "baz",
},
wantBodyIncludes: []string{"500", "Internal Server Error"},
},
"show details": {
giveConfig: func() *config.Config {
cfg := config.New()
cfg.ShowDetails = true
return &cfg
},
giveUrl: "http://example.com/503",
giveHeaders: map[string]string{
"Accept": "application/json",
"X-Original-URI": "/foo/bar",
"X-Namespace": "some-Namespace",
"X-Ingress-Name": "ingress-name",
"X-Service-Name": "service-name",
"X-Service-Port": "666",
"X-Request-ID": "req-id-777",
"X-Forwarded-For": "123.123.123.123:12312",
},
wantStatusCode: http.StatusOK,
wantHeaders: map[string]string{"Content-Type": "application/json; charset=utf-8"},
wantBodyIncludes: []string{
"503",
"Service Unavailable",
"details",
"/foo/bar",
"some-Namespace",
"ingress-name",
"service-name",
"666",
"req-id-777",
"123.123.123.123:12312",
"example.com",
},
},
"fallback to StatusText if code is not found": {
giveConfig: func() *config.Config {
cfg := config.New()
cfg.Codes = config.Codes{}
return &cfg
},
giveUrl: "http://testing/100",
giveHeaders: map[string]string{"Accept": "application/json"},
wantStatusCode: http.StatusOK,
wantHeaders: map[string]string{"Content-Type": "application/json; charset=utf-8"},
wantBodyIncludes: []string{"100", "Continue"},
},
"unknown code": {
giveConfig: func() *config.Config {
cfg := config.New()
cfg.Codes = config.Codes{}
return &cfg
},
giveUrl: "http://testing/1",
giveHeaders: map[string]string{"Accept": "application/json"},
wantStatusCode: http.StatusOK,
wantHeaders: map[string]string{"Content-Type": "application/json; charset=utf-8"},
wantBodyIncludes: []string{"1", "Unknown Status Code"},
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
var handler = error_page.New(tt.giveConfig(), logger.NewNop())
req, reqErr := http.NewRequest(http.MethodGet, tt.giveUrl, http.NoBody)
require.NoError(t, reqErr)
for k, v := range tt.giveHeaders {
req.Header.Set(k, v)
}
httptest.HandleFastRequest(t, handler, req, func(status int, body string, headers http.Header) {
assert.Equal(t, tt.wantStatusCode, status)
for hName, hWant := range tt.wantHeaders {
for hGot := range headers {
if hGot == hName {
assert.Contains(t, hWant, headers.Get(hGot))
}
}
}
for _, wantBodyInclude := range tt.wantBodyIncludes {
assert.Contains(t, body, wantBodyInclude)
}
})
})
}
}
func TestRotationModeOnEachRequest(t *testing.T) {
t.Parallel()
var cfg = config.New()
cfg.RotationMode = config.RotationModeRandomOnEachRequest
cfg.Templates = map[string]string{
"foo": "foo",
"bar": "bar",
}
var (
lastResponseBody string
changedTimes int
handler = error_page.New(&cfg, logger.NewNop())
)
for range 300 {
req, reqErr := http.NewRequest(http.MethodGet, "http://testing/", http.NoBody)
require.NoError(t, reqErr)
req.Header.Set("Accept", "text/html")
httptest.HandleFastRequest(t, handler, req, func(status int, body string, headers http.Header) {
if lastResponseBody != body {
changedTimes++
lastResponseBody = body
}
})
}
assert.True(t, changedTimes > 30, "the template should be changed at least 30 times")
}

View File

@ -1,34 +0,0 @@
package errorpage
import (
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/http/core"
"github.com/tarampampam/error-pages/internal/options"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/valyala/fasthttp"
)
type (
templatePicker interface {
// Pick the template name for responding.
Pick() string
}
renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
)
// NewHandler creates handler for error pages serving.
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
core.SetClientFormat(ctx, core.PlainTextContentType) // default content type
if code, ok := ctx.UserValue("code").(string); ok {
core.RespondWithErrorPage(ctx, cfg, p, rdr, code, fasthttp.StatusOK, opt)
} else { // will never occur
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot extract requested code from the request")
}
}
}

View File

@ -1,7 +0,0 @@
package errorpage_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,24 +0,0 @@
package healthz
import "github.com/valyala/fasthttp"
// checker allows to check some service part.
type checker interface {
// Check makes a check and return error only if something is wrong.
Check() error
}
// NewHandler creates healthcheck handler.
func NewHandler(checker checker) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
if err := checker.Check(); err != nil {
ctx.SetStatusCode(fasthttp.StatusServiceUnavailable)
_, _ = ctx.WriteString(err.Error())
return
}
ctx.SetStatusCode(fasthttp.StatusOK)
_, _ = ctx.WriteString("OK")
}
}

View File

@ -1,7 +0,0 @@
package healthz_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -1,49 +0,0 @@
package index
import (
"strconv"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/http/core"
"github.com/tarampampam/error-pages/internal/options"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/valyala/fasthttp"
)
type (
templatePicker interface {
// Pick the template name for responding.
Pick() string
}
renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error)
}
)
// NewHandler creates handler for the index page serving.
func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, opt options.ErrorPage) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
pageCode, httpCode := opt.Default.PageCode, int(opt.Default.HTTPCode)
if returnCode, ok := extractCodeToReturn(ctx); ok {
pageCode, httpCode = strconv.Itoa(returnCode), returnCode
}
core.RespondWithErrorPage(ctx, cfg, p, rdr, pageCode, httpCode, opt)
}
}
func extractCodeToReturn(ctx *fasthttp.RequestCtx) (int, bool) { // for the Ingress support
var ch = ctx.Request.Header.Peek(core.CodeHeader)
if len(ch) > 0 && len(ch) <= 3 {
if code, err := strconv.Atoi(string(ch)); err == nil {
if code > 0 && code <= 599 {
return code, true
}
}
}
return 0, false
}

View File

@ -1,7 +0,0 @@
package index_test
import "testing"
func TestNothing(t *testing.T) {
t.Skip("tests for this package have not been implemented yet")
}

View File

@ -0,0 +1,30 @@
package live
import (
"net/http"
"github.com/valyala/fasthttp"
)
// New creates a new handler that returns "OK" for GET and HEAD requests.
func New() fasthttp.RequestHandler {
var (
body = []byte("OK\n")
notAllowed = http.StatusText(http.StatusMethodNotAllowed) + "\n"
)
return func(ctx *fasthttp.RequestCtx) {
switch string(ctx.Method()) {
case fasthttp.MethodGet:
ctx.SetContentType("text/plain; charset=utf-8")
ctx.SetStatusCode(http.StatusOK)
_, _ = ctx.Write(body)
case fasthttp.MethodHead:
ctx.SetStatusCode(http.StatusOK)
default:
ctx.Error(notAllowed, http.StatusMethodNotAllowed)
}
}
}

View File

@ -0,0 +1,52 @@
package live_test
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"gh.tarampamp.am/error-pages/internal/http/handlers/live"
"gh.tarampamp.am/error-pages/internal/http/httptest"
)
func TestServeHTTP(t *testing.T) {
t.Parallel()
var (
handler = live.New()
url = "http://testing"
body = http.NoBody
)
t.Run("get", func(t *testing.T) {
httptest.HandleFast(t, handler, http.MethodGet, url, body, func(status int, body string, headers http.Header) {
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, "text/plain; charset=utf-8", headers.Get("Content-Type"))
assert.Equal(t, "OK\n", body)
})
})
t.Run("head", func(t *testing.T) {
httptest.HandleFast(t, handler, http.MethodHead, url, body, func(status int, body string, headers http.Header) {
assert.Equal(t, http.StatusOK, status)
assert.Empty(t, headers.Get("Content-Type"))
assert.Empty(t, body)
})
})
t.Run("method not allowed", func(t *testing.T) {
for _, method := range []string{
http.MethodDelete,
http.MethodPatch,
http.MethodPost,
http.MethodPut,
} {
httptest.HandleFast(t, handler, method, url, body, func(status int, body string, headers http.Header) {
assert.Equal(t, http.StatusMethodNotAllowed, status)
assert.Equal(t, "text/plain; charset=utf-8", headers.Get("Content-Type"))
assert.Equal(t, "Method Not Allowed\n", body)
})
}
})
}

View File

@ -1,16 +0,0 @@
// Package metrics contains HTTP handler for application metrics (prometheus format) generation.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
)
// NewHandler creates metrics handler.
func NewHandler(registry prometheus.Gatherer) fasthttp.RequestHandler {
return fasthttpadaptor.NewFastHTTPHandler(
promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}),
)
}

Some files were not shown because too many files have changed in this diff Show More