v2: App rewritten in Go (#25)

This commit is contained in:
Paramtamtam 2021-09-29 20:38:50 +05:00 committed by GitHub
parent ce98410e51
commit 29f024ebcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 3923 additions and 716 deletions

26
.codecov.yml Normal file
View File

@ -0,0 +1,26 @@
# 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,7 +1,14 @@
.dockerignore
Dockerfile
.github
.git
.gitignore
/generator/node_modules
/generator/*.log
/out
.editorconfig
.idea
.vscode
test
temp
tmp
LICENSE
Makefile
error-pages

View File

@ -1,3 +1,5 @@
# EditorConfig docs: <https://editorconfig.org/>
root = true
[*]
@ -11,5 +13,5 @@ trim_trailing_whitespace = true
[*.{yml, yaml, sh, conf}]
indent_size = 2
[Makefile]
[{Makefile, go.mod, *.go}]
indent_style = tab

View File

@ -5,100 +5,96 @@ on:
types: [published]
jobs:
demo:
name: Update demonstration, hosted on github pages
build:
name: Build for ${{ matrix.os }} (${{ matrix.arch }})
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
os: [linux, darwin] # linux, freebsd, darwin, windows
arch: [amd64] # amd64, 386
steps:
- name: Check out code
uses: actions/checkout@v2
- uses: actions/setup-go@v2
with: {go-version: 1.17.1}
- name: Setup NodeJS
uses: actions/setup-node@v1 # Action page: <https://github.com/actions/setup-node>
- uses: actions/checkout@v2
- 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:
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 }}
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
with:
node-version: 15
- uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Install dependencies
working-directory: generator
run: yarn install
- name: Generate pages
run: ./generator/generator.js -i -c ./config.json -o ./out
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: content
path: out/
retention-days: 1
- name: Switch to github pages branch
uses: actions/checkout@v2
with:
ref: gh-pages
- name: Download artifact
uses: actions/download-artifact@v2
with:
name: content
- name: Setup git
run: |
git config --global user.name "$GITHUB_ACTOR"
git config --global user.email 'actions@github.com'
git remote add github "https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git"
- name: Stage changes
run: git add .
- name: Commit changes
run: git commit --allow-empty -m "Deploying ${GITHUB_SHA} to Github Pages"
- name: Push changes
run: git push github --force
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
steps:
- name: Check out code
uses: actions/checkout@v2
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1 # Action page: <https://github.com/docker/setup-qemu-action>
- uses: gacts/github-slug@v1
id: slug
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 # Action page: <https://github.com/docker/setup-buildx-action>
- uses: docker/setup-qemu-action@v1 # Action page: <https://github.com/docker/setup-qemu-action>
- name: Login to default Container Registry
uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-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>
with:
username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
- uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_PASSWORD }}
- name: Generate builder values
id: values
run: echo "::set-output name=version::${GITHUB_REF##*/[vV]}" # `/refs/tags/v1.2.3` -> `1.2.3`
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action>
with:
context: .
file: Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
build-args: "APP_VERSION=${{ steps.slug.outputs.version }}"
tags: |
tarampampam/error-pages:${{ steps.slug.outputs.version }}
tarampampam/error-pages:latest
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
- name: Build image
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 buildx build \
--platform "linux/amd64,linux/arm64/v8,linux/arm/v6,linux/arm/v7" \
--tag "tarampampam/error-pages:${{ steps.values.outputs.version }}" \
--tag "tarampampam/error-pages:latest" \
--tag "ghcr.io/${{ github.actor }}/error-pages:${{ steps.values.outputs.version }}" \
--tag "ghcr.io/${{ github.actor }}/error-pages:latest" \
--file ./Dockerfile \
--push \
.
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

View File

@ -2,54 +2,130 @@ name: tests
on:
push:
branches:
- master
tags-ignore:
- '**'
paths-ignore:
- '**.md'
branches: [master, main]
tags-ignore: ['**']
paths-ignore: ['**.md']
pull_request:
paths-ignore:
- '**.md'
paths-ignore: ['**.md']
jobs: # Docs: <https://git.io/JvxXE>
gitleaks:
name: Gitleaks
runs-on: ubuntu-20.04
steps:
- name: Check out code
uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/checkout@v2
with: {fetch-depth: 0}
- name: Check for GitLeaks
uses: zricethezav/gitleaks-action@v1.5.0 # Action page: <https://github.com/zricethezav/gitleaks-action>
- uses: zricethezav/gitleaks-action@v1 # Action page: <https://github.com/zricethezav/gitleaks-action>
generate:
name: Try to run generator
golangci-lint:
name: Golang-CI (lint)
runs-on: ubuntu-20.04
steps:
- name: Check out code
uses: actions/checkout@v2
- uses: actions/checkout@v2
- name: Setup NodeJS
uses: actions/setup-node@v1 # Action page: <https://github.com/actions/setup-node>
- name: Run linter
uses: golangci/golangci-lint-action@v2 # Action page: <https://github.com/golangci/golangci-lint-action>
with:
node-version: 15
version: v1.42 # without patch version
only-new-issues: false # show only new issues if it's a pull request
- uses: actions/cache@v2
go-test:
name: Unit tests
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with: {go-version: 1.17}
- uses: actions/checkout@v2
with: {fetch-depth: 2} # Fixes codecov error 'Issue detecting commit SHA'
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
uses: actions/cache@v2
id: go-cache
with:
path: '**/node_modules'
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- name: Install dependencies
working-directory: generator
run: yarn install
- 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 }}
build:
name: Build for ${{ matrix.os }} (${{ matrix.arch }})
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
os: [linux, darwin] # linux, freebsd, darwin, windows
arch: [amd64] # amd64, 386
needs: [golangci-lint, go-test]
steps:
- uses: actions/setup-go@v2
with: {go-version: 1.17}
- uses: actions/checkout@v2
- uses: gacts/github-slug@v1
id: slug
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-
- run: go mod download
- name: Build application
env:
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
CGO_ENABLED: 0
LDFLAGS: -s -w -X github.com/tarampampam/error-pages/internal/version.version=${{ steps.slug.outputs.branch-name-slug }}@${{ steps.slug.outputs.commit-hash-short }}
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@v2
- 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: ./generator/generator.js -i -c ./config.json -o ./out
run: ./error-pages build ./out --verbose --index
- name: Test file creation
- name: Test files creation
run: |
test -f ./out/index.html
test -f ./out/ghost/404.html
@ -59,31 +135,76 @@ jobs: # Docs: <https://git.io/JvxXE>
test -f ./out/noise/404.html
test -f ./out/hacker-terminal/404.html
docker-build:
docker-image:
name: Build docker image
runs-on: ubuntu-20.04
needs: [golangci-lint, go-test]
steps:
- name: Check out code
uses: actions/checkout@v2
- uses: actions/checkout@v2
- name: Build docker image
run: docker build -f ./Dockerfile --tag image:local .
- uses: gacts/github-slug@v1
id: slug
- name: Scan image
uses: anchore/scan-action@v2 # action page: <https://github.com/anchore/scan-action>
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action>
with:
image: image:local
context: .
file: Dockerfile
push: false
build-args: "APP_VERSION=${{ steps.slug.outputs.branch-name-slug }}@${{ 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/download-artifact@v2
with:
name: docker-image
path: .artifact
- working-directory: .artifact
run: docker load < docker-image.tar
- uses: anchore/scan-action@v3 # action page: <https://github.com/anchore/scan-action>
with:
image: app:ci
fail-build: true
severity-cutoff: medium # negligible, low, medium, high or critical
severity-cutoff: low # negligible, low, medium, high or critical
- name: Run docker image
run: docker run --rm -d -p "8080:8080/tcp" -e "TEMPLATE_NAME=ghost" image:local
poke-docker-image:
name: Run the docker image
runs-on: ubuntu-20.04
needs: [docker-image]
timeout-minutes: 2
steps:
- uses: actions/download-artifact@v2
with:
name: docker-image
path: .artifact
- name: Pause
run: sleep 2
- working-directory: .artifact
run: docker load < docker-image.tar
- name: Verify 500.html error file exists in root
run: curl -sS --fail "http://127.0.0.1:8080/500.html"
- name: Run container with the app
run: docker run --rm -d -p "8080:8080/tcp" --name app app:ci
- name: Verify root request HTTP code
run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/) -eq 404
- name: Wait for container "healthy" state
run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done
- run: curl --fail http://127.0.0.1:8080/500.html
- run: curl --fail http://127.0.0.1:8080/400.html
- run: curl --fail http://127.0.0.1:8080/health/live
- run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/) -eq 404
- name: Stop the container
if: always()
run: docker kill app

13
.gitignore vendored
View File

@ -1,9 +1,16 @@
## IDEs
/.vscode
/.idea
/.vscode
## Dist
/out
## Binaries
error-pages
## Temp dirs & trash
/temp
/tmp
*.env
.DS_Store
*.cache
*.out
/out
/cover*.*

96
.golangci.yml Normal file
View File

@ -0,0 +1,96 @@
# Documentation: <https://github.com/golangci/golangci-lint#config-file>
run:
timeout: 1m
skip-dirs:
- .github
- .git
modules-download-mode: readonly
allow-parallel-runners: true
output:
format: colored-line-number # colored-line-number|line-number|json|tab|checkstyle|code-climate
linters-settings:
govet:
check-shadowing: true
gocyclo:
min-complexity: 15
godot:
scope: declarations
capital: true
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 3
misspell:
locale: US
lll:
line-length: 120
prealloc:
simple: true
range-loops: true
for-loops: true
nolintlint:
allow-leading-space: false
require-specific: true
linters: # All available linters list: <https://golangci-lint.run/usage/linters/>
disable-all: true
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bodyclose # Checks whether HTTP response body is closed successfully
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- 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
- errorlint # find code that will cause problems with the error wrapping scheme introduced in Go 1.13
- exhaustive # check exhaustiveness of enum switch statements
- exportloopref # checks for pointers to enclosing loop variables
- funlen # Tool for detection of long functions
- 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
- 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
- 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
- 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!
issues:
exclude-rules:
- path: _test\.go
linters:
- dupl
- funlen
- scopelint
- gocognit
- noctx

View File

@ -4,6 +4,12 @@ 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.0.0
### Changed
- Application rewritten in Go
## v1.8.0
### Added

View File

@ -1,52 +1,76 @@
# Image page: <https://hub.docker.com/_/node>
FROM node:15.14-alpine as builder
# syntax=docker/dockerfile:1.2
# copy required sources into builder image
COPY ./generator /src/generator
COPY ./config.json /src
COPY ./templates /src/templates
COPY ./docker /src/docker
# Image page: <https://hub.docker.com/_/golang>
FROM golang:1.17.1-alpine as builder
# install generator dependencies
WORKDIR /src/generator
RUN yarn install --frozen-lockfile --no-progress --non-interactive
# 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"
# run generator
WORKDIR /src
RUN ./generator/generator.js -c ./config.json -o ./out
COPY . .
# arguments to pass on each go tool link invocation
ENV LDFLAGS="-s -w -X github.com/tarampampam/error-pages/internal/version.version=$APP_VERSION"
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
WORKDIR /tmp/rootfs
# prepare rootfs for runtime
RUN mkdir /tmp/rootfs
WORKDIR /tmp/rootfs
RUN set -x \
&& mkdir -p \
./docker-entrypoint.d \
./etc/nginx/conf.d \
./opt \
&& mv /src/out ./opt/html \
&& echo -e "User-agent: *\nDisallow: /\n" > ./opt/html/robots.txt \
&& touch ./opt/html/favicon.ico \
&& mv /src/docker/docker-entrypoint.d/* ./docker-entrypoint.d \
&& mv /src/docker/nginx-server.conf ./etc/nginx/conf.d/default.conf
./etc \
./bin \
./opt/html \
&& 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 \
&& mv /src/error-pages.yml ./opt/error-pages.yml
# Image page: <https://hub.docker.com/_/nginx>
FROM nginx:1.21.1-alpine as runtime
WORKDIR /tmp/rootfs/opt
# generate static error pages (for usage inside another docker images, for example)
RUN set -x \
&& ./../bin/error-pages --config-file ./error-pages.yml build ./html --verbose --index \
&& ls -l ./html
# use empty filesystem
FROM scratch as runtime
ARG APP_VERSION="undefined@docker"
LABEL \
# 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 docker image" \
org.opencontainers.image.description="Static server error pages in the docker image" \
org.opencontainers.image.url="https://github.com/tarampampam/error-pages" \
org.opencontainers.image.source="https://github.com/tarampampam/error-pages" \
org.opencontainers.image.vendor="tarampampam" \
org.opencontainers.version="$APP_VERSION" \
org.opencontainers.image.licenses="MIT"
# Import from builder
COPY --from=builder /tmp/rootfs /
# Use an unprivileged user
USER appuser:appuser
WORKDIR /opt
ENV LISTEN_PORT="8080" \
TEMPLATE_NAME="ghost"
# Docs: <https://docs.docker.com/engine/reference/builder/#healthcheck>
HEALTHCHECK --interval=15s --timeout=2s --retries=2 --start-period=2s CMD [ \
"wget", "--spider", "-q", "http://127.0.0.1:8080/health/live" \
HEALTHCHECK --interval=7s --timeout=2s CMD [ \
"/bin/error-pages", "healthcheck", "--log-json" \
]
RUN chown -R nginx:nginx /opt/html
ENTRYPOINT ["/bin/error-pages"]
CMD ["serve", "--log-json"]

View File

@ -3,27 +3,59 @@
# 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 install gen preview
.PHONY : help \
image dive build fmt lint gotest 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%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[32m%-11s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
install: ## Install all dependencies
docker-compose run $(DC_RUN_ARGS) -w "/src/generator" node yarn install --frozen-lockfile --no-progress --non-interactive
gen: ## Generate error pages
docker-compose run $(DC_RUN_ARGS) node nodejs ./generator/generator.js -i -c ./config.json -o ./out
preview: ## Build docker image and start preview
image: ## Build docker image with app
docker build -f ./Dockerfile -t $(APP_NAME):local .
@printf "\n \e[30;42m %s \033[0m\n\n" 'Now open in your favorite browser <http://127.0.0.1:8081> and press CTRL+C for stopping'
docker run --rm -i -p 8081:8080 -e "TEMPLATE_NAME=random" $(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 ...`';
shell: ## Start shell into container with node
docker-compose run $(DC_RUN_ARGS) node sh
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
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/
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
lint: ## Run app linters
docker-compose run --rm --no-deps golint golangci-lint run
gotest: ## Run app tests
docker-compose run $(DC_RUN_ARGS) --no-deps app go test -v -race -timeout 10s ./...
test: lint gotest ## Run app tests and linters
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

286
README.md
View File

@ -2,62 +2,190 @@
<img src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png" width="94" alt="" />
</p>
# HTTP's error pages in Docker image
# HTTP's error pages
[![Build Status][badge_build_status]][link_build_status]
[![Release version][badge_release_version]][link_releases]
![Project language][badge_language]
[![Build Status][badge_build]][link_build]
[![Release Status][badge_release]][link_build]
[![Coverage][badge_coverage]][link_coverage]
[![Image size][badge_size_latest]][link_docker_hub]
[![Docker Pulls][badge_docker_pulls]][link_docker_hub]
[![License][badge_license]][link_license]
This repository contains:
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:
- A very simple [generator](generator/generator.js) _(`nodejs`)_ for HTTP error pages _(like `404: Not found`)_ with different templates supports
- Dockerfile for docker image ([docker hub][link_docker_hub], [ghcr.io][link_ghcr]) with generated pages and `nginx` as a web server
- Simple error pages generator, written on Go
- Single-page error page templates with different designs (located in the [templates](templates) directory)
- Fast and lightweight HTTP server (written on Go also, with the [FastHTTP][fasthttp] under the hood)
- Already generated error pages (sources can be [found here][preview-sources], the **demonstration** is always accessible [here][preview-demo])
- Lightweight docker image _(~3.5Mb compressed size)_ with all the things described above
**Can be used for [Traefik error pages customization](https://docs.traefik.io/middlewares/errorpages/)**.
Also, this project can be used for the [**Traefik** error pages customization](https://doc.traefik.io/traefik/middlewares/http/errorpages/).
## Demo
<p align="center">
<img src="https://hsto.org/webt/bc/bt/9i/bcbt9i3jyvozequr1e4maz7i2q8.png" alt="" />
</p>
Generated pages (from the latest release) always **[accessible here][link_gh_pages]** _(live preview)_.
## Installing
## Templates
Name | Preview
:---------------: | :-----:
`ghost` | [![ghost](https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif)](https://tarampampam.github.io/error-pages/ghost/404.html)
`l7-light` | [![l7-light](https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png)](https://tarampampam.github.io/error-pages/l7-light/404.html)
`l7-dark` | [![l7-dark](https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png)](https://tarampampam.github.io/error-pages/l7-dark/404.html)
`shuffle` | [![shuffle](https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif)](https://tarampampam.github.io/error-pages/shuffle/404.html)
`noise` | [![noise](https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif)](https://tarampampam.github.io/error-pages/noise/404.html)
`hacker-terminal` | [![hacker-terminal](https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif)](https://tarampampam.github.io/error-pages/hacker-terminal/404.html)
> Note: `noise` template highly uses the CPU, be careful
## Usage
Generated error pages in our [docker image][link_docker_hub] permanently located in directory `/opt/html/%TEMPLATE_NAME%`. `nginx` in a container listen for `8080` (`http`) port.
#### Supported environment variables
Name | Description
--------------- | -----------
`TEMPLATE_NAME` | (`ghost` by default) "default" pages template _(allows to use error pages without passing theme name in URL - `http://127.0.0.1/500.html` instead `http://127.0.0.1/ghost/500.html`)_
Also, you can use a special template name `random` - in this case template will be selected randomly.
### Ready docker image
Download the latest binary file for your os/arch from the [releases page][link_releases] or use our docker image:
[![image stats](https://dockeri.co/image/tarampampam/error-pages)][link_docker_hub]
Execute in your shell:
Registry | Image
-------------------------------------- | -----
[Docker Hub][link_docker_hub] | `tarampampam/error-pages`
[GitHub Container Registry][link_ghcr] | `ghcr.io/tarampampam/error-pages`
> Using the `latest` tag for the docker image is highly discouraged because of possible backward-incompatible changes during **major** upgrades. Please, use tags in `X.Y.Z` format
To watch the docker image content you can use the [dive](https://github.com/wagoodman/dive):
```bash
$ docker run --rm -p "8082:8080" tarampampam/error-pages:X.X.X
$ docker run --rm -it \
-v "/var/run/docker.sock:/var/run/docker.sock:ro" \
wagoodman/dive:latest \
tarampampam/error-pages:latest
```
> Important notice: do **not** use the `latest` image tag _(this is bad practice)_. Use versioned tag (like `1.2.3`) instead. Docker hub mirror located [here (ghcr.io)][link_ghcr].
<details>
<summary>Dive screenshot</summary>
And open in your browser `http://127.0.0.1:8082/ghost/400.html`.
<p align="center">
<img src="https://hsto.org/webt/mi/ak/uf/miakufsh2ibxtsa1nomfhyqombi.png" alt="" />
</p>
</details>
## Usage
All of the examples below will use a docker image with the application, but you can also use a binary. By the way, our docker image uses the **unleveled user** by default and **distroless**.
### HTTP server
As mentioned above - our application can be run as an HTTP server. It only needs to specify the path to the configuration file (it does not need statically generated error pages). The server uses [FastHTTP][fasthttp] and stores all necessary data in memory - so it does not use the file system and very fast. Oh yes, the image with the app also contains a configured **healthcheck** and **logs in JSON** format :)
For the HTTP server running execute in your terminal:
```bash
$ docker run --rm \
-p "8080:8080/tcp" \
-e "TEMPLATE_NAME=random" \
tarampampam/error-pages
```
And open [`http://127.0.0.1:8080/404.html`](http://127.0.0.1:8080/404.html) in your favorite browser. Error pages are accessible by the following URLs: `http://127.0.0.1:8080/{page_code}.html`.
Environment variable `TEMPLATE_NAME` should be used for the theme switching (supported templates are described below).
> **Cheat**: you can use `random` (to use the randomized theme on server start) or `i-said-random` (to use the randomized template on **each request**)
To see the help run the following command:
```bash
$ docker run --rm tarampampam/error-pages serve --help
```
### Generator
Create a config file (`error-pages.yml`) with the following content:
```yaml
templates:
- path: ./foo.html # Template name "foo" (same as file name),
# content located in the file "foo.html"
- name: bar # Template name "bar", its content is described below:
content: "Error {{ code }}: {{ message }} ({{ description }})"
pages:
400:
message: Bad Request
description: The server did not understand the request
401:
message: Unauthorized
description: The requested page needs a username and a password
```
Template file `foo.html`:
```html
<html>
<title>{{ code }}</title>
<body>
<h1>{{ message }}: {{ description }}</h1>
</body>
</html>
```
And run the generator:
```bash
$ docker run --rm \
-v "$(pwd):/opt:rw" \
-u "$(id -u):$(id -g)" \
tarampampam/error-pages build --config-file ./error-pages.yml ./out
$ tree
.
├── error-pages.yml
├── foo.html
└── out
├── bar
│ ├── 400.html
│ └── 401.html
└── foo
├── 400.html
└── 401.html
3 directories, 6 files
$ cat ./out/foo/400.html
<html>
<title>400</title>
<body>
<h1>Bad Request: The server did not understand the request</h1>
</body>
</html>
$ cat ./out/bar/400.html
Error 400: Bad Request (The server did not understand the request)
```
To see the usage help run the following command:
```bash
$ docker run --rm tarampampam/error-pages build --help
```
### Static error pages
You may want to use the generated error pages somewhere else, and you can simply extract them from the docker image to your local directory for this purpose:
```bash
$ docker create --name error-pages tarampampam/error-pages:2.0.0-rc2
$ docker cp error-pages:/opt/html ./out
$ docker rm -f error-pages
$ ls ./out
ghost hacker-terminal index.html l7-dark l7-light noise shuffle
$ tree
.
└── out
├── ghost
│ ├── 400.html
│ ├── ...
│ └── 505.html
├── hacker-terminal
│ ├── 400.html
│ ├── ...
│ └── 505.html
├── index.html
├── l7-dark
│ ├── 400.html
│ ├── ...
...
```
### Custom error pages for your image with [nginx][link_nginx]
@ -92,12 +220,12 @@ server {
```dockerfile
# File `Dockerfile`
FROM nginx:1.18-alpine
FROM nginx:1.21-alpine
COPY --chown=nginx \
./nginx.conf /etc/nginx/conf.d/default.conf
COPY --chown=nginx \
--from=tarampampam/error-pages:1.7.0 \
--from=tarampampam/error-pages:2.0.0 \
/opt/html/ghost /usr/share/nginx/errorpages/_error-pages
```
@ -107,16 +235,29 @@ $ docker build --tag your-nginx:local -f ./Dockerfile .
> More info about `error_page` directive can be [found here](http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page).
### Custom error pages for [Traefik][link_traefik]
## Templates
Simple traefik (tested on `v2.4.8`) service configuration for usage in [docker swarm][link_swarm] (**change with your needs**):
Name | Preview
:---------------: | :-----:
`ghost` | [![ghost](https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif)](https://tarampampam.github.io/error-pages/ghost/404.html)
`l7-light` | [![l7-light](https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png)](https://tarampampam.github.io/error-pages/l7-light/404.html)
`l7-dark` | [![l7-dark](https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png)](https://tarampampam.github.io/error-pages/l7-dark/404.html)
`shuffle` | [![shuffle](https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif)](https://tarampampam.github.io/error-pages/shuffle/404.html)
`noise` | [![noise](https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif)](https://tarampampam.github.io/error-pages/noise/404.html)
`hacker-terminal` | [![hacker-terminal](https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif)](https://tarampampam.github.io/error-pages/hacker-terminal/404.html)
> Note: `noise` template highly uses the CPU, be careful
## Custom error pages for [Traefik][link_traefik]
Simple traefik (tested on `v2.5.3`) service configuration for usage in [docker swarm][link_swarm] (**change with your needs**):
```yaml
version: '3.4'
version: '3.8'
services:
error-pages:
image: tarampampam/error-pages:1.7.0
image: tarampampam/error-pages:2.0.0
environment:
TEMPLATE_NAME: l7-dark
networks:
@ -164,28 +305,6 @@ networks:
external: true
```
## Development
> For project development we use `docker-ce` + `docker-compose`. Make sure you have them installed.
Install "generator" dependencies:
```bash
$ make install
```
If you want to generate error pages on your machine _(after that look into the output directory)_:
```bash
$ make gen
```
If you want to preview the pages using the Docker image:
```bash
$ make preview
```
## Changes log
[![Release date][badge_release_date]][link_releases]
@ -204,24 +323,35 @@ If you will find any package errors, please, [make an issue][link_create_issue]
This is open-sourced software licensed under the [MIT License][link_license].
[badge_build_status]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests/master
[badge_release_date]:https://img.shields.io/github/release-date/tarampampam/error-pages.svg?style=flat-square&maxAge=180
[badge_commits_since_release]:https://img.shields.io/github/commits-since/tarampampam/error-pages/latest.svg?style=flat-square&maxAge=180
[badge_issues]:https://img.shields.io/github/issues/tarampampam/error-pages.svg?style=flat-square&maxAge=180
[badge_pulls]:https://img.shields.io/github/issues-pr/tarampampam/error-pages.svg?style=flat-square&maxAge=180
[badge_license]:https://img.shields.io/github/license/tarampampam/error-pages.svg?longCache=true
[badge_build]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests?maxAge=30&label=tests&logo=github
[badge_release]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/release?maxAge=30&label=release&logo=github
[badge_coverage]:https://img.shields.io/codecov/c/github/tarampampam/error-pages/master.svg?maxAge=30
[badge_release_version]:https://img.shields.io/github/release/tarampampam/error-pages.svg?maxAge=30
[badge_size_latest]:https://img.shields.io/docker/image-size/tarampampam/error-pages/latest?maxAge=30
[badge_docker_pulls]:https://img.shields.io/docker/pulls/tarampampam/error-pages.svg
[badge_language]:https://img.shields.io/github/go-mod/go-version/tarampampam/error-pages?longCache=true
[badge_license]:https://img.shields.io/github/license/tarampampam/error-pages.svg?longCache=true
[badge_release_date]:https://img.shields.io/github/release-date/tarampampam/error-pages.svg?maxAge=180
[badge_commits_since_release]:https://img.shields.io/github/commits-since/tarampampam/error-pages/latest.svg?maxAge=45
[badge_issues]:https://img.shields.io/github/issues/tarampampam/error-pages.svg?maxAge=45
[badge_pulls]:https://img.shields.io/github/issues-pr/tarampampam/error-pages.svg?maxAge=45
[link_coverage]:https://codecov.io/gh/tarampampam/error-pages
[link_build]:https://github.com/tarampampam/error-pages/actions
[link_docker_hub]:https://hub.docker.com/r/tarampampam/error-pages
[link_docker_tags]:https://hub.docker.com/r/tarampampam/error-pages/tags
[link_license]:https://github.com/tarampampam/error-pages/blob/master/LICENSE
[link_releases]:https://github.com/tarampampam/error-pages/releases
[link_commits]:https://github.com/tarampampam/error-pages/commits
[link_changes_log]:https://github.com/tarampampam/error-pages/blob/master/CHANGELOG.md
[link_issues]:https://github.com/tarampampam/error-pages/issues
[link_create_issue]:https://github.com/tarampampam/error-pages/issues/new/choose
[link_pulls]:https://github.com/tarampampam/error-pages/pulls
[link_build_status]:https://travis-ci.org/tarampampam/error-pages
[link_create_issue]:https://github.com/tarampampam/error-pages/issues/new
[link_license]:https://github.com/tarampampam/error-pages/blob/master/LICENSE
[link_docker_hub]:https://hub.docker.com/r/tarampampam/error-pages/
[link_ghcr]:https://github.com/users/tarampampam/packages/container/package/error-pages
[fasthttp]:https://github.com/valyala/fasthttp
[preview-sources]:https://github.com/tarampampam/error-pages/tree/gh-pages
[preview-demo]:https://tarampampam.github.io/error-pages/
[link_nginx]:http://nginx.org/
[link_traefik]:https://docs.traefik.io/
[link_swarm]:https://docs.docker.com/engine/swarm/

29
cmd/error-pages/main.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"os"
"path/filepath"
"github.com/fatih/color"
"github.com/tarampampam/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()) }
// run this CLI application.
// Exit codes documentation: <https://tldp.org/LDP/abs/html/exitcodes.html>
func run() int {
cmd := cli.NewCommand(filepath.Base(os.Args[0]))
if err := cmd.Execute(); err != nil {
_, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error())
return 1
}
return 0
}

View File

@ -1,133 +0,0 @@
{
"templates": [
{
"name": "ghost",
"path": "./templates/ghost.html"
},
{
"name": "l7-light",
"path": "./templates/l7-light.html"
},
{
"name": "l7-dark",
"path": "./templates/l7-dark.html"
},
{
"name": "shuffle",
"path": "./templates/shuffle.html"
},
{
"name": "noise",
"path": "./templates/noise.html"
},
{
"name": "hacker-terminal",
"path": "./templates/hacker-terminal.html"
}
],
"output": {
"file_extension": "html"
},
"pages": [
{
"code": 400,
"message": "Bad Request",
"description": "The server did not understand the request"
},
{
"code": 401,
"message": "Unauthorized",
"description": "The requested page needs a username and a password"
},
{
"code": 403,
"message": "Forbidden",
"description": "Access is forbidden to the requested page"
},
{
"code": 404,
"message": "Not Found",
"description": "The server can not find the requested page"
},
{
"code": 405,
"message": "Method Not Allowed",
"description": "The method specified in the request is not allowed"
},
{
"code": 407,
"message": "Proxy Authentication Required",
"description": "You must authenticate with a proxy server before this request can be served"
},
{
"code": 408,
"message": "Request Timeout",
"description": "The request took longer than the server was prepared to wait"
},
{
"code": 409,
"message": "Conflict",
"description": "The request could not be completed because of a conflict"
},
{
"code": 410,
"message": "Gone",
"description": "The requested page is no longer available"
},
{
"code": 411,
"message": "Length Required",
"description": "The \"Content-Length\" is not defined. The server will not accept the request without it"
},
{
"code": 412,
"message": "Precondition Failed",
"description": "The pre condition given in the request evaluated to false by the server"
},
{
"code": 413,
"message": "Payload Too Large",
"description": "The server will not accept the request, because the request entity is too large"
},
{
"code": 416,
"message": "Requested Range Not Satisfiable",
"description": "The requested byte range is not available and is out of bounds"
},
{
"code": 418,
"message": "I'm a teapot",
"description": "Attempt to brew coffee with a teapot is not supported"
},
{
"code": 429,
"message": "Too Many Requests",
"description": "Too many requests in a given amount of time"
},
{
"code": 500,
"message": "Internal Server Error",
"description": "The server met an unexpected condition"
},
{
"code": 502,
"message": "Bad Gateway",
"description": "The server received an invalid response from the upstream server"
},
{
"code": 503,
"message": "Service Unavailable",
"description": "The server is temporarily overloading or down"
},
{
"code": 504,
"message": "Gateway Timeout",
"description": "The gateway has timed out"
},
{
"code": 505,
"message": "HTTP Version Not Supported",
"description": "The server does not support the \"http protocol\" version"
}
]
}

View File

@ -1,16 +1,46 @@
version: '3.2'
# Docker-compose file is used only for local development. This is not production-ready example.
version: '3.4'
volumes:
tmp-data:
tmp-data: {}
golint-cache: {}
services:
node:
image: node:15.14-alpine # Image page: <https://hub.docker.com/_/node>
app: &app-service
image: golang:1.17.1-buster # Image page: <https://hub.docker.com/_/golang>
working_dir: /src
environment:
HOME: /tmp
PS1: '\[\033[1;32m\]\[\033[1;36m\][\u@docker] \[\033[1;34m\]\w\[\033[0;35m\] \[\033[1;36m\]# \[\033[0m\]'
GOPATH: /tmp
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
- .:/src:cached
- .:/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
healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/health/live']
interval: 5s
timeout: 2s
golint:
image: golangci/golangci-lint:v1.42-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

View File

@ -1,41 +0,0 @@
#!/usr/bin/env sh
set -e
# on `docker restart` next directory keep existing: <https://github.com/tarampampam/error-pages/issues/3>
if [ -d /opt/html/nginx-error-pages ]; then
rm -Rf /opt/html/nginx-error-pages;
fi;
# allows to use random template
if [ ! -z "$TEMPLATE_NAME" ] && ([ "$TEMPLATE_NAME" = "random" ] || [ "$TEMPLATE_NAME" = "RANDOM" ]); then
# find all templates in directory (only template directories must be located in /opt/html)
allowed_templates=$(find /opt/html/* -maxdepth 1 -type d -exec basename {} \;);
# pick random template name
random_template_name=$(shuf -e -n1 $allowed_templates);
echo "$0: Use '$random_template_name' as randomly selected template";
TEMPLATE_NAME="$random_template_name"
fi;
TEMPLATE_NAME=${TEMPLATE_NAME:-ghost} # string|empty
echo "$0: Set pages for template '$TEMPLATE_NAME' as default (make accessible in root directory)";
# check for template existing
if [ ! -d "/opt/html/$TEMPLATE_NAME" ]; then
echo >&3 "$0: Template '$TEMPLATE_NAME' was not found!";
exit 1;
fi;
# allows "direct access" to the error pages using URLs like "/500.html"
ln -f -s "/opt/html/$TEMPLATE_NAME/"* /opt/html;
# next directory is required for easy nginx `error_page` usage
mkdir /opt/html/nginx-error-pages;
# use error pages from the template as "native" nginx error pages
ln -f -s "/opt/html/$TEMPLATE_NAME/"* /opt/html/nginx-error-pages;
exit 0

View File

@ -1,32 +0,0 @@
server {
listen 8080;
server_name _;
server_tokens off;
index index.html index.htm;
root /opt/html;
error_page 400 /nginx-error-pages/400.html;
error_page 401 /nginx-error-pages/401.html;
error_page 403 /nginx-error-pages/403.html;
error_page 404 /nginx-error-pages/404.html;
error_page 500 /nginx-error-pages/500.html;
error_page 502 /nginx-error-pages/502.html;
location ^~ /nginx-error-pages/ {
internal;
root /opt/html;
}
# Health-check (liveness probe)
location = /health/live {
access_log off;
default_type text/plain;
return 200 "healthy\n";
}
location / {
try_files $uri =404;
}
}

93
error-pages.yml Normal file
View File

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

View File

@ -1,9 +0,0 @@
## Vendors
/node_modules
## Lock files (use yarn only)
package-lock.json
## Temp dirs & trash
npm-debug.log
yarn-error.log

View File

@ -1,130 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const yargs = require('yargs');
const options = yargs
.usage('Usage: -c <config.json> -d <output-directory>')
.option("c", {alias: "config", describe: "config file path", type: "string", demandOption: true})
.option("o", {alias: "out", describe: "output directory path", type: "string", demandOption: true})
.option("i", {alias: "index", describe: "generate index page", type: "boolean"})
.argv;
const configFile = options.config;
const outDir = options.out;
const generateIndexPage = options.index;
const generated = {};
try {
// Make sure that config file exists
if (!fs.existsSync(configFile)) {
throw new Error(`Config file "${configFile}" was not found`);
}
// Create output directory (if needed)
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir);
}
// Read JSON config file and parse into object
const configContent = JSON.parse(fs.readFileSync(configFile));
// Loop over all defined templates in configuration file
configContent.templates.forEach((templateConfig) => {
// Make sure that template layout file exists
if (!fs.existsSync(templateConfig.path)) {
throw new Error(`Template "${templateConfig.name}" was not found in "${templateConfig.path}"`);
}
// Read layout content into memory prepare output directory for template
const layoutContent = String(fs.readFileSync(templateConfig.path));
const templateOutDir = path.join(outDir, templateConfig.name);
if (!fs.existsSync(templateOutDir)) {
fs.mkdirSync(templateOutDir);
}
console.info(`Use template "${templateConfig.name}" located in "${templateConfig.path}"`);
// Loop over all pages
configContent.pages.forEach((pageConfig) => {
let outFileName = pageConfig.code + "." + configContent.output.file_extension,
outPath = path.join(templateOutDir, outFileName);
console.info(` [${templateConfig.name}:${pageConfig.code}] Output: ${outPath}`);
// Make replaces
let result = layoutContent
.replace(/{{\s?code\s?}}/g, pageConfig.code)
.replace(/{{\s?message\s?}}/g, pageConfig.message)
.replace(/{{\s?description\s?}}/g, pageConfig.description);
// And write into result file
fs.writeFileSync(outPath, result, {
encoding: "utf8",
flag: "w+",
mode: 0o644
});
if (!generated[templateConfig.name]) {
generated[templateConfig.name] = [];
}
generated[templateConfig.name].push({
code: pageConfig.code,
message: pageConfig.message,
description: pageConfig.description,
path: path.join(templateConfig.name, outFileName),
})
});
})
// Generate index page for the generated content
if (generateIndexPage === true) {
const indexPageFilePath = path.join(outDir, 'index.html');
let content = `<!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/4.6.0/css/bootstrap.min.css"
integrity="sha512-P5MgMn1jBN01asBgU0z60Qk4QxiXo86+wlFahKrsQf37c9cro517WzVSPPV1tDKzhku2iJ2FVgL67wG03SGnNA=="
crossorigin="anonymous" />
</head>
<body>
<main role="main" class="container">\n`;
Object.keys(generated).forEach(function(templateName) {
content += `<h2 class="mt-5">Template name: <code>` + templateName + `</code></h2>\n<ul class="mb-5">\n`;
generated[templateName].forEach((properties) => {
content += ` <li><a href="${properties.path}"><span class="badge badge-light">${properties.code}</span>: ${properties.message}</a></li>\n`;
})
content += `</ul>\n`;
});
content += `</main>
<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>`;
fs.writeFileSync(indexPageFilePath, content, {
encoding: "utf8",
flag: "w+",
mode: 0o644
});
}
} catch (err) {
console.error(err);
process.exit(1);
}

View File

@ -1,10 +0,0 @@
{
"name": "error-pages",
"repository": {
"type": "git",
"url": "git://github.com/tarampampam/error-pages.git"
},
"dependencies": {
"yargs": "16.2"
}
}

View File

@ -1,109 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
ansi-regex@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
string-width@^4.1.0, string-width@^4.2.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
strip-ansi@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
dependencies:
ansi-regex "^5.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yargs-parser@^20.2.2:
version "20.2.7"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
yargs@16.2:
version "16.2.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.0"
y18n "^5.0.5"
yargs-parser "^20.2.2"

32
go.mod Normal file
View File

@ -0,0 +1,32 @@
module github.com/tarampampam/error-pages
go 1.17
require (
github.com/a8m/envsubst v1.2.0
github.com/fasthttp/router v1.4.3
github.com/fatih/color v1.7.0
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d
github.com/pkg/errors v0.8.1
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/valyala/fasthttp v1.30.0
go.uber.org/zap v1.19.1
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
require (
github.com/andybalholm/brotli v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
)

615
go.sum Normal file
View File

@ -0,0 +1,615 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/a8m/envsubst v1.2.0 h1:yvzAhJD2QKdo35Ut03wIfXQmg+ta3wC/1bskfZynz+Q=
github.com/a8m/envsubst v1.2.0/go.mod h1:PpvLvNWa+Rvu/10qXmFbFiGICIU5hZvFJNPCCkUaObg=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fasthttp/router v1.4.3 h1:spS+LUnRryQ/+hbmYzs/xWGJlQCkeQI3hxGZdlVYhLU=
github.com/fasthttp/router v1.4.3/go.mod h1:9ytWCfZ5LcCcbD3S7pEXyBX9vZnOZmN918WiiaYUzr8=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0=
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/savsgio/gotils v0.0.0-20210907153846-c06938798b52/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8=
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g=
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -0,0 +1,54 @@
// Package breaker provides OSSignals struct for OS signals handling (with context).
package breaker
import (
"context"
"os"
"os/signal"
"syscall"
)
// OSSignals allows to subscribe 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 of 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

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

@ -0,0 +1,56 @@
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/health/live", 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

@ -0,0 +1,48 @@
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/health/live", 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")
}

10
internal/checkers/live.go Normal file
View File

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

@ -0,0 +1,12 @@
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())
}

View File

@ -0,0 +1,198 @@
package build
import (
"bytes"
"os"
"path"
"text/template"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/tarampampam/error-pages/internal/config"
"github.com/tarampampam/error-pages/internal/tpl"
"go.uber.org/zap"
)
type historyItem struct {
Code, Message, Path string
}
// NewCommand creates `build` command.
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:funlen,gocognit
var (
generateIndex bool
cfg *config.Config
)
cmd := &cobra.Command{
Use: "build <output-directory>",
Aliases: []string{"b"},
Short: "Build the error pages",
Args: cobra.ExactArgs(1),
PreRunE: func(*cobra.Command, []string) error {
if configFile == nil {
return errors.New("path to the config file is required for this command")
}
if c, err := config.FromYamlFile(*configFile); err != nil {
return err
} else {
if err = c.Validate(); err != nil {
return err
}
cfg = c
}
return nil
},
RunE: func(_ *cobra.Command, args []string) error {
if len(args) != 1 {
return errors.New("wrong arguments count")
}
log.Info("loading templates")
templates, err := cfg.LoadTemplates()
if err != nil {
return err
} else if len(templates) == 0 {
return errors.New("no loaded templates")
}
log.Debug("the output directory preparing", zap.String("Path", args[0]))
if err = createDirectory(args[0]); err != nil {
return errors.Wrap(err, "cannot prepare output directory")
}
codes := make(map[string]tpl.Annotator)
for code, desc := range cfg.Pages {
codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
}
history := make(map[string][]historyItem, len(templates))
log.Info("saving the error pages")
startedAt := time.Now()
if err = tpl.NewErrors(templates, codes).VisitAll(func(template, code string, content []byte) error {
if e := createDirectory(path.Join(args[0], template)); e != nil {
return e
}
fileName := code + ".html"
if e := os.WriteFile(path.Join(args[0], template, fileName), content, 0664); e != nil { //nolint:gosec,gomnd
return e
}
if _, ok := history[template]; !ok {
history[template] = make([]historyItem, 0, len(codes))
}
history[template] = append(history[template], historyItem{
Code: code,
Message: codes[code].Message,
Path: path.Join(template, fileName),
})
return nil
}); err != nil {
return nil
}
log.Debug("saved", zap.Duration("duration", time.Since(startedAt)))
if generateIndex {
log.Info("index file generation")
startedAt = time.Now()
if err = writeIndexFile(path.Join(args[0], "index.html"), history); err != nil {
return err
}
log.Debug("index file generated", zap.Duration("duration", time.Since(startedAt)))
}
return nil
},
}
cmd.Flags().BoolVarP(
&generateIndex,
"index", "i",
false,
"generate index page",
)
return cmd
}
func createDirectory(path string) error {
stat, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(path, 0775) //nolint:gomnd
}
return err
}
if !stat.IsDir() {
return errors.New("is not a directory")
}
return nil
}
func writeIndexFile(path string, history map[string][]historyItem) error {
t, err := template.New("index").Parse(`<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Error pages list</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.1/css/bootstrap.min.css"
integrity="sha512-6KY5s6UI5J7SVYuZB4S/CZMyPylqyyNZco376NM2Z8Sb8OxEdp02e1jkKk/wZxIEmjQ6DRCEBhni+gpr9c4tvA=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body class="bg-light">
<div class="container">
<main>
<div class="py-5 text-center">
<img class="d-block mx-auto mb-4" src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png"
alt="" width="94">
<h2>Error pages index</h2>
</div>
{{- range $template, $item := . -}}
<h2 class="mb-3">Template name: <Code>{{ $template }}</Code></h2>
<ul class="mb-5">
{{ range $item -}}
<li><a href="{{ .Path }}"><strong>{{ .Code }}</strong>: {{ .Message }}</a></li>
{{ end -}}
</ul>
{{ end }}
</main>
</div>
<footer class="footer">
<div class="container text-center text-muted mt-3 mb-3">
For online documentation and support please refer to the
<a href="https://github.com/tarampampam/error-pages">project repository</a>.
</div>
</footer>
</body>
</html>`)
if err != nil {
return err
}
var buf bytes.Buffer
if err = t.Execute(&buf, history); err != nil {
return err
}
return os.WriteFile(path, buf.Bytes(), 0664) //nolint:gosec,gomnd
}

View File

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

View File

@ -0,0 +1,57 @@
// Package healthcheck contains CLI `healthcheck` command implementation.
package healthcheck
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/tarampampam/error-pages/internal/env"
)
type checker interface {
Check(port uint16) error
}
const portFlagName = "port"
// NewCommand creates `healthcheck` command.
func NewCommand(checker checker) *cobra.Command {
var port uint16
cmd := &cobra.Command{
Use: "healthcheck",
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
},
RunE: func(*cobra.Command, []string) error {
return checker.Check(port)
},
}
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

@ -0,0 +1,94 @@
package healthcheck_test
import (
"errors"
"os"
"testing"
"github.com/kami-zh/go-capturer"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/cli/healthcheck"
)
type fakeChecker struct{ err error }
func (c *fakeChecker) Check(port uint16) error { return c.err }
func TestProperties(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
assert.Equal(t, "healthcheck", cmd.Use)
assert.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases)
assert.NotNil(t, cmd.RunE)
}
func TestCommandRun(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
cmd.SetArgs([]string{})
output := capturer.CaptureOutput(func() {
assert.NoError(t, cmd.Execute())
})
assert.Empty(t, output)
}
func TestCommandRunFailed(t *testing.T) {
cmd := healthcheck.NewCommand(&fakeChecker{err: errors.New("foo err")})
cmd.SetArgs([]string{})
output := capturer.CaptureStderr(func() {
assert.Error(t, cmd.Execute())
})
assert.Contains(t, output, "foo err")
}
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)
}

93
internal/cli/root.go Normal file
View File

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

84
internal/cli/root_test.go Normal file
View File

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

@ -0,0 +1,148 @@
package serve
import (
"context"
"errors"
"os"
"time"
"github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
"github.com/tarampampam/error-pages/internal/tpl"
"go.uber.org/zap"
"github.com/spf13/cobra"
"github.com/tarampampam/error-pages/internal/breaker"
"github.com/tarampampam/error-pages/internal/config"
appHttp "github.com/tarampampam/error-pages/internal/http"
)
// NewCommand creates `serve` command.
func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command {
var (
f flags
cfg *config.Config
)
cmd := &cobra.Command{
Use: "serve",
Aliases: []string{"s", "server"},
Short: "Start HTTP server",
PreRunE: func(cmd *cobra.Command, _ []string) 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 c, err := config.FromYamlFile(*configFile); err != nil {
return err
} else {
if err = c.Validate(); err != nil {
return err
}
cfg = c
}
return f.validate()
},
RunE: func(*cobra.Command, []string) error { return run(ctx, log, f, cfg) },
}
f.init(cmd.Flags())
return cmd
}
const serverShutdownTimeout = 15 * time.Second
// run current command.
func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) 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
}()
// load templates content
templates, loadingErr := cfg.LoadTemplates()
if loadingErr != nil {
return loadingErr
} else if len(templates) == 0 {
return errors.New("no loaded templates")
}
if f.template.name != "" && f.template.name != errorpage.UseRandom && f.template.name != errorpage.UseRandomOnEachRequest { //nolint:lll
if _, found := templates[f.template.name]; !found {
return errors.New("requested nonexistent template: " + f.template.name) // requested unknown template
}
}
// burn the error codes map
codes := make(map[string]tpl.Annotator)
for code, desc := range cfg.Pages {
codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
}
// create HTTP server
server := appHttp.NewServer(log)
// register server routes, middlewares, etc.
if err := server.Register(f.template.name, templates, codes); err != nil {
return err
}
startingErrCh := make(chan error, 1) // channel for server starting error
// start HTTP server in separate goroutine
go func(errCh chan<- error) {
defer close(errCh)
log.Info("Server starting",
zap.String("addr", f.listen.ip),
zap.Uint16("port", f.listen.port),
zap.String("template name", f.template.name),
)
if err := server.Start(f.listen.ip, f.listen.port); err != nil {
errCh <- err
}
}(startingErrCh)
// and wait for...
select {
case err := <-startingErrCh: // ..server starting error
return err
case <-ctx.Done(): // ..or context cancellation
log.Info("Gracefully server stopping")
stoppedAt := time.Now()
// stop the server using created context above
if err := server.Stop(serverShutdownTimeout); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Error("Server stopping timeout exceeded", zap.Duration("timeout", serverShutdownTimeout))
}
return err
}
log.Debug("Server stopped", zap.Duration("stopping duration", time.Since(stoppedAt)))
}
return nil
}

View File

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

View File

@ -0,0 +1,91 @@
package serve
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
"github.com/spf13/pflag"
"github.com/tarampampam/error-pages/internal/env"
)
type flags struct {
listen struct {
ip string
port uint16
}
template struct {
name string
}
}
const (
listenFlagName = "listen"
portFlagName = "port"
templateNameFlagName = "template-name"
)
func (f *flags) init(flagSet *pflag.FlagSet) {
flagSet.StringVarP(
&f.listen.ip,
listenFlagName, "l",
"0.0.0.0",
fmt.Sprintf("IP address to listen on [$%s]", env.ListenAddr),
)
flagSet.Uint16VarP(
&f.listen.port,
portFlagName, "p",
8080, //nolint:gomnd // must be same as default healthcheck `--port` flag value
fmt.Sprintf("TCP port number [$%s]", env.ListenPort),
)
flagSet.StringVarP(
&f.template.name,
templateNameFlagName, "t",
"",
fmt.Sprintf(
"template name (set \"%s\" to use the randomized or \"%s\" to use the randomized template on each request) [$%s]", //nolint:lll
errorpage.UseRandom, errorpage.UseRandomOnEachRequest, env.TemplateName,
),
)
}
func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) {
flagSet.VisitAll(func(flag *pflag.Flag) {
// flag was NOT defined using CLI (flags should have maximal priority)
if !flag.Changed { //nolint:nestif
switch flag.Name {
case listenFlagName:
if envVar, exists := env.ListenAddr.Lookup(); exists {
f.listen.ip = strings.TrimSpace(envVar)
}
case portFlagName:
if envVar, exists := env.ListenPort.Lookup(); exists {
if p, err := strconv.ParseUint(envVar, 10, 16); err == nil { //nolint:gomnd
f.listen.port = uint16(p)
} else {
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar)
}
}
case templateNameFlagName:
if envVar, exists := env.TemplateName.Lookup(); exists {
f.template.name = strings.TrimSpace(envVar)
}
}
}
})
return lastErr
}
func (f *flags) validate() error {
if net.ParseIP(f.listen.ip) == nil {
return fmt.Errorf("wrong IP address [%s] for listening", f.listen.ip)
}
return nil
}

View File

@ -0,0 +1,24 @@
// 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

@ -0,0 +1,30 @@
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())
}

116
internal/config/config.go Normal file
View File

@ -0,0 +1,116 @@
package config
import (
"io/ioutil"
"path/filepath"
"strconv"
"strings"
"github.com/a8m/envsubst"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
type Config struct {
Templates []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
} `yaml:"templates"`
Pages map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
} `yaml:"pages"`
}
// Validate the config and return an error if something is wrong.
func (c Config) Validate() error {
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")
}
}
}
return nil
}
// LoadTemplates loading templates content from the local files and return it.
func (c Config) LoadTemplates() (map[string][]byte, error) {
var templates = make(map[string][]byte)
for i := 0; i < len(c.Templates); i++ {
var name string
if c.Templates[i].Name == "" {
basename := filepath.Base(c.Templates[i].Path)
name = strings.TrimSuffix(basename, filepath.Ext(basename))
} else {
name = c.Templates[i].Name
}
var content []byte
if c.Templates[i].Content == "" {
b, err := ioutil.ReadFile(c.Templates[i].Path)
if err != nil {
return nil, errors.Wrap(err, "cannot load content for the template "+name)
}
content = b
} else {
content = []byte(c.Templates[i].Content)
}
templates[name] = content
}
return templates, nil
}
// FromYaml creates new config instance using YAML-structured content.
func FromYaml(in []byte) (cfg *Config, err error) {
cfg = &Config{}
in, err = envsubst.Bytes(in)
if err != nil {
return nil, err
}
if err = yaml.Unmarshal(in, cfg); err != nil {
return nil, errors.Wrap(err, "cannot parse configuration file")
}
return
}
// 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")
}
return FromYaml(bytes)
}

View File

@ -0,0 +1,315 @@
package config_test
import (
"errors"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/config"
)
func TestConfig_Validate(t *testing.T) {
for name, tt := range map[string]struct {
giveConfig func() config.Config
wantError error
}{
"valid": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{"foo", "bar", "baz"},
}
c.Pages = map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
}{
"400": {"Bad Request", "The server did not understand the request"},
}
return c
},
wantError: nil,
},
"empty templates list": {
giveConfig: func() config.Config {
return config.Config{}
},
wantError: errors.New("empty templates list"),
},
"empty path and name": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{
Path: "foo",
Name: "bar",
Content: "baz",
},
{
Path: "",
Name: "",
Content: "blah",
},
}
return c
},
wantError: errors.New("empty path and name with index 1"),
},
"empty path and template content": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{
Path: "foo",
Name: "bar",
Content: "baz",
},
{
Path: "",
Name: "blah",
Content: "",
},
}
return c
},
wantError: errors.New("empty path and template content with index 1"),
},
"empty pages list": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{"foo", "bar", "baz"},
}
c.Pages = map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
}{}
return c
},
wantError: errors.New("empty pages list"),
},
"empty page code": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{"foo", "bar", "baz"},
}
c.Pages = map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
}{
"": {"foo", "bar"},
}
return c
},
wantError: errors.New("empty page code"),
},
"code should not contain whitespaces": {
giveConfig: func() config.Config {
c := config.Config{}
c.Templates = []struct {
Path string `yaml:"path"`
Name string `yaml:"name"`
Content string `yaml:"content"`
}{
{"foo", "bar", "baz"},
}
c.Pages = map[string]struct {
Message string `yaml:"message"`
Description string `yaml:"description"`
}{
" 123": {"foo", "bar"},
}
return c
},
wantError: errors.New("code should not contain whitespaces"),
},
} {
tt := tt
t.Run(name, func(t *testing.T) {
err := tt.giveConfig().Validate()
if tt.wantError != nil {
assert.EqualError(t, err, tt.wantError.Error())
} else {
assert.NoError(t, err)
}
})
}
}
func TestFromYaml(t *testing.T) {
var cases = []struct { //nolint:maligned
name string
giveYaml []byte
giveEnv map[string]string
wantErr bool
checkResultFn func(*testing.T, *config.Config)
}{
{
name: "with all possible values",
giveEnv: map[string]string{
"__GHOST_PATH": "./templates/ghost.html",
"__GHOST_NAME": "ghost",
},
giveYaml: []byte(`
templates:
- path: ${__GHOST_PATH}
name: ${__GHOST_NAME:-default_value} # name is optional
- path: ./templates/l7-light.html
- name: Foo
content: |
Some content
New line
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
`),
wantErr: false,
checkResultFn: func(t *testing.T, cfg *config.Config) {
assert.Len(t, cfg.Templates, 3)
assert.Equal(t, "./templates/ghost.html", cfg.Templates[0].Path)
assert.Equal(t, "ghost", cfg.Templates[0].Name)
assert.Equal(t, "", cfg.Templates[0].Content)
assert.Equal(t, "./templates/l7-light.html", cfg.Templates[1].Path)
assert.Equal(t, "", cfg.Templates[1].Name)
assert.Equal(t, "", cfg.Templates[1].Content)
assert.Equal(t, "", cfg.Templates[2].Path)
assert.Equal(t, "Foo", cfg.Templates[2].Name)
assert.Equal(t, "Some content\nNew line\n", cfg.Templates[2].Content)
assert.Len(t, cfg.Pages, 2)
assert.Equal(t, "Bad Request", cfg.Pages["400"].Message)
assert.Equal(t, "The server did not understand the request", cfg.Pages["400"].Description)
assert.Equal(t, "Unauthorized", cfg.Pages["401"].Message)
assert.Equal(t, "The requested page needs a username and a password", cfg.Pages["401"].Description)
},
},
{
name: "broken yaml",
giveYaml: []byte(`foo bar`),
wantErr: true,
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.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 = []struct { //nolint:maligned
name string
giveYamlFilePath string
wantErr bool
checkResultFn func(*testing.T, *config.Config)
}{
{
name: "with all possible values",
giveYamlFilePath: "./testdata/simple.yml",
wantErr: false,
checkResultFn: func(t *testing.T, cfg *config.Config) {
assert.Len(t, cfg.Templates, 2)
assert.Equal(t, "./templates/ghost.html", cfg.Templates[0].Path)
assert.Equal(t, "ghost", cfg.Templates[0].Name)
assert.Equal(t, "./templates/l7-light.html", cfg.Templates[1].Path)
assert.Equal(t, "", cfg.Templates[1].Name)
assert.Len(t, cfg.Pages, 2)
assert.Equal(t, "Bad Request", cfg.Pages["400"].Message)
assert.Equal(t, "The server did not understand the request", cfg.Pages["400"].Description)
assert.Equal(t, "Unauthorized", cfg.Pages["401"].Message)
assert.Equal(t, "The requested page needs a username and a password", cfg.Pages["401"].Description)
},
},
{
name: "broken yaml",
giveYamlFilePath: "./testdata/broken.yml",
wantErr: true,
},
{
name: "wrong file path",
giveYamlFilePath: "foo bar",
wantErr: true,
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.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)
}
})
}
}

1
internal/config/testdata/broken.yml vendored Normal file
View File

@ -0,0 +1 @@
foo bar

13
internal/config/testdata/simple.yml vendored Normal file
View File

@ -0,0 +1,13 @@
templates:
- path: ./templates/ghost.html
name: ghost # name is optional
- path: ./templates/l7-light.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

21
internal/env/env.go vendored Normal file
View File

@ -0,0 +1,21 @@
// 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
)
// 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)) }

45
internal/env/env_test.go vendored Normal file
View File

@ -0,0 +1,45 @@
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))
}
func TestEnvVariable_Lookup(t *testing.T) {
cases := []struct {
giveEnv envVariable
}{
{giveEnv: ListenAddr},
{giveEnv: ListenPort},
{giveEnv: TemplateName},
{giveEnv: ConfigFilePath},
}
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

@ -0,0 +1,35 @@
package common
import (
"strings"
"github.com/valyala/fasthttp"
)
const internalErrorPattern = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" />
<title>Internal error occurred</title>
<style>
html,body {background-color: #0e0e0e;color:#fff;font-family:'Nunito',sans-serif;height:100%;margin:0}
.message {height:100%;align-items:center;display:flex;justify-content:center;position:relative;font-size:1.4em}
img {padding-right: .4em}
</style>
</head>
<body>
<div class="message">
<img src="https://hsto.org/webt/fs/sx/gt/fssxgtssfg689qxboqvjil5yz8g.png" alt="logo" height="32">
<p>{{ message }}</p>
</div>
</body>
</html>`
func HandleInternalHTTPError(ctx *fasthttp.RequestCtx, statusCode int, message string) {
ctx.SetStatusCode(statusCode)
ctx.SetContentType("text/html; charset=UTF-8")
_, _ = ctx.WriteString(strings.ReplaceAll(internalErrorPattern, "{{ message }}", message))
}

View File

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

View File

@ -0,0 +1,33 @@
package common
import (
"strings"
"time"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
var (
startedAt = time.Now()
ua = string(ctx.UserAgent())
)
h(ctx)
if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging
return
}
log.Info("HTTP request processed",
zap.String("useragent", ua),
zap.String("method", string(ctx.Method())),
zap.String("url", string(ctx.RequestURI())),
zap.Int("status_code", ctx.Response.StatusCode()),
zap.Bool("connection_close", ctx.Response.ConnectionClose()),
zap.Duration("duration", time.Since(startedAt)),
)
}
}

View File

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

View File

@ -0,0 +1,85 @@
package errorpage
import (
"math/rand"
"sort"
"time"
"github.com/pkg/errors"
"github.com/tarampampam/error-pages/internal/http/common"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/valyala/fasthttp"
)
const (
UseRandom = "random"
UseRandomOnEachRequest = "i-said-random"
)
// NewHandler creates handler for error pages serving.
func NewHandler(
templateName string,
templates map[string][]byte,
codes map[string]tpl.Annotator,
) (fasthttp.RequestHandler, error) {
if len(templates) == 0 {
return nil, errors.New("empty templates map")
}
var (
rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
templateNames = templateTames(templates)
)
if templateName == "" { // on empty template name
templateName = templateNames[0] // pick the first
} else if templateName == UseRandom { // on "random" template name
templateName = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
}
if _, found := templates[templateName]; !found && templateName != UseRandomOnEachRequest {
return nil, errors.New("wrong template name passed")
}
var pages = tpl.NewErrors(templates, codes)
return func(ctx *fasthttp.RequestCtx) {
var useTemplate = templateName // default
if templateName == UseRandomOnEachRequest {
useTemplate = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
}
if code, ok := ctx.UserValue("code").(string); ok {
if content, err := pages.Get(useTemplate, code); err == nil {
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetContentType("text/html; charset=utf-8")
_, _ = ctx.Write(content)
} else {
common.HandleInternalHTTPError(
ctx,
fasthttp.StatusNotFound,
"requested code not available: "+err.Error(),
)
}
} else { // will never happen
common.HandleInternalHTTPError(
ctx,
fasthttp.StatusInternalServerError,
"cannot extract requested code from the request",
)
}
}, nil
}
func templateTames(templates map[string][]byte) []string {
var templateNames = make([]string, 0, len(templates))
for name := range templates {
templateNames = append(templateNames, name)
}
sort.Strings(templateNames)
return templateNames
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
package version
import (
"encoding/json"
"github.com/valyala/fasthttp"
)
// NewHandler creates version handler.
func NewHandler(ver string) fasthttp.RequestHandler {
var cache []byte
return func(ctx *fasthttp.RequestCtx) {
if cache == nil {
cache, _ = json.Marshal(struct {
Version string `json:"version"`
}{
Version: ver,
})
}
ctx.SetContentType("application/json")
ctx.SetStatusCode(fasthttp.StatusOK)
_, _ = ctx.Write(cache)
}
}

View File

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

106
internal/http/server.go Normal file
View File

@ -0,0 +1,106 @@
package http
import (
"context"
"strconv"
"time"
"github.com/fasthttp/router"
"github.com/tarampampam/error-pages/internal/checkers"
"github.com/tarampampam/error-pages/internal/http/common"
errorpageHandler "github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
healthzHandler "github.com/tarampampam/error-pages/internal/http/handlers/healthz"
versionHandler "github.com/tarampampam/error-pages/internal/http/handlers/version"
"github.com/tarampampam/error-pages/internal/tpl"
"github.com/tarampampam/error-pages/internal/version"
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)
type Server struct {
fast *fasthttp.Server
router *router.Router
}
const (
defaultWriteTimeout = time.Second * 7
defaultReadTimeout = time.Second * 7
defaultIdleTimeout = time.Second * 15
)
func NewServer(log *zap.Logger) Server {
r := router.New()
return Server{
// fasthttp docs: <https://github.com/valyala/fasthttp>
fast: &fasthttp.Server{
WriteTimeout: defaultWriteTimeout,
ReadTimeout: defaultReadTimeout,
IdleTimeout: defaultIdleTimeout,
Handler: common.LogRequest(r.Handler, log),
NoDefaultServerHeader: true,
ReduceMemoryUsage: true,
CloseOnShutdown: true,
Logger: zap.NewStdLog(log),
},
router: r,
}
}
// Start server.
func (s *Server) Start(ip string, port uint16) error {
return s.fast.ListenAndServe(ip + ":" + strconv.Itoa(int(port)))
}
// Register server routes, middlewares, etc.
// Router docs: <https://github.com/fasthttp/router>
func (s *Server) Register(
templateName string,
templates map[string][]byte,
codes map[string]tpl.Annotator,
) error {
s.router.GET("/", func(ctx *fasthttp.RequestCtx) {
common.HandleInternalHTTPError(
ctx,
fasthttp.StatusNotFound,
"Hi there! Error pages are available at the following URLs: /{code}.html",
)
})
s.router.NotFound = func(ctx *fasthttp.RequestCtx) {
common.HandleInternalHTTPError(
ctx,
fasthttp.StatusNotFound,
"Wrong request URL. Error pages are available at the following URLs: /{code}.html",
)
}
s.router.GET("/version", versionHandler.NewHandler(version.Version()))
s.router.ANY("/health/live", healthzHandler.NewHandler(checkers.NewLiveChecker()))
if h, err := errorpageHandler.NewHandler(templateName, templates, codes); err != nil {
return err
} else {
s.router.GET("/{code}.html", h)
}
return nil
}
// Stop server.
func (s *Server) Stop(timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout) // TODO replace with simple time.After
defer cancel()
ch := make(chan error, 1) // channel for server stopping error
go func() { defer close(ch); ch <- s.fast.Shutdown() }()
select {
case err := <-ch:
return err
case <-ctx.Done():
return ctx.Err()
}
}

View File

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

38
internal/logger/logger.go Normal file
View File

@ -0,0 +1,38 @@
// Package logger contains functions for a working with application logging.
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// New creates new "zap" logger with little customization.
func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
var config zap.Config
if logJSON {
config = zap.NewProductionConfig()
} else {
config = zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05")
}
// default configuration for all encoders
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
config.Development = false
config.DisableStacktrace = true
config.DisableCaller = true
if debug {
config.Development = true
config.DisableStacktrace = false
config.DisableCaller = false
}
if verbose || debug {
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
}
return config.Build()
}

View File

@ -0,0 +1,80 @@
package logger_test
import (
"regexp"
"strings"
"testing"
"time"
"github.com/kami-zh/go-capturer"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/logger"
)
func TestNewNotVerboseDebugJSON(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(false, false, false)
assert.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg")
log.Error("err msg")
})
assert.Contains(t, output, time.Now().Format("15:04:05"))
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
assert.NotContains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewVerboseNotDebugJSON(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(true, false, false)
assert.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg")
log.Error("err msg")
})
assert.Contains(t, output, time.Now().Format("15:04:05"))
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
assert.Contains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewVerboseDebugNotJSON(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(true, true, false)
assert.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg")
log.Error("err msg")
})
assert.Contains(t, output, time.Now().Format("15:04:05"))
assert.Regexp(t, `\t.+info.+\t.+logger_test\.go:\d+\tinf msg`, output)
assert.Contains(t, output, "dbg msg")
assert.Contains(t, output, "err msg")
}
func TestNewNotVerboseDebugButJSON(t *testing.T) {
output := capturer.CaptureStderr(func() {
log, err := logger.New(false, false, true)
assert.NoError(t, err)
log.Info("inf msg")
log.Debug("dbg msg")
log.Error("err msg")
})
// replace timestamp field with fixed value
fakeTimestamp := regexp.MustCompile(`"ts":\d+\.\d+,`)
output = fakeTimestamp.ReplaceAllString(output, `"ts":0.1,`)
lines := strings.Split(strings.Trim(output, "\n"), "\n")
assert.JSONEq(t, `{"level":"info","ts":0.1,"msg":"inf msg"}`, lines[0])
assert.JSONEq(t, `{"level":"error","ts":0.1,"msg":"err msg"}`, lines[1])
}

102
internal/tpl/errors.go Normal file
View File

@ -0,0 +1,102 @@
package tpl
import (
"errors"
"sync"
)
// Annotator allows to annotate error code.
type Annotator struct {
Message string
Description string
}
// Errors is a "cached storage" for the rendered error pages for the different templates and codes.
type Errors struct {
templates map[string][]byte
codes map[string]Annotator
cacheMu sync.RWMutex
cache map[string]map[string][]byte // map[template]map[code]content
}
// NewErrors creates new Errors.
func NewErrors(templates map[string][]byte, codes map[string]Annotator) *Errors {
return &Errors{
templates: templates,
codes: codes,
cache: make(map[string]map[string][]byte),
}
}
func (e *Errors) existsInCache(template, code string) ([]byte, bool) {
e.cacheMu.RLock()
defer e.cacheMu.RUnlock()
if codes, tplOk := e.cache[template]; tplOk {
if content, codeOk := codes[code]; codeOk {
return content, true
}
}
return nil, false
}
func (e *Errors) putInCache(template, code string) error {
if _, ok := e.templates[template]; !ok {
return errors.New("template \"" + template + "\" does not exists")
}
if _, ok := e.codes[code]; !ok {
return errors.New("code \"" + code + "\" does not exists")
}
e.cacheMu.Lock()
defer e.cacheMu.Unlock()
if _, ok := e.cache[template]; !ok {
e.cache[template] = make(map[string][]byte)
}
e.cache[template][code] = Replace(e.templates[template], Replaces{
Code: code,
Message: e.codes[code].Message,
Description: e.codes[code].Description,
})
return nil
}
// Get the rendered error page content.
func (e *Errors) Get(template, code string) ([]byte, error) {
if content, ok := e.existsInCache(template, code); ok {
return content, nil
}
if err := e.putInCache(template, code); err != nil {
return nil, err
}
e.cacheMu.RLock()
defer e.cacheMu.RUnlock()
return e.cache[template][code], nil
}
// VisitAll allows to iterate all possible error pages and templates.
func (e *Errors) VisitAll(fn func(template, code string, content []byte) error) error {
for tpl := range e.templates {
for code := range e.codes {
content, err := e.Get(tpl, code)
if err != nil {
return err // will never happen
}
if err = fn(tpl, code, content); err != nil {
return err
}
}
}
return nil
}

106
internal/tpl/errors_test.go Normal file
View File

@ -0,0 +1,106 @@
package tpl_test
import (
"errors"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/tpl"
)
func TestErrors_Get(t *testing.T) {
e := tpl.NewErrors(
map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
)
content, err := e.Get("foo", "200")
assert.NoError(t, err)
assert.Equal(t, "200: ok all is ok", string(content))
content, err = e.Get("foo", "666")
assert.EqualError(t, err, "code \"666\" does not exists")
assert.Nil(t, content)
content, err = e.Get("bar", "200")
assert.EqualError(t, err, "template \"bar\" does not exists")
assert.Nil(t, content)
}
func TestErrors_GetConcurrent(t *testing.T) {
e := tpl.NewErrors(
map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
)
var wg sync.WaitGroup
for i := 0; i < 1234; i++ {
wg.Add(1)
go func() {
defer wg.Done()
content, err := e.Get("foo", "200")
assert.NoError(t, err)
assert.Equal(t, "200: ok all is ok", string(content))
content, err = e.Get("foo", "666")
assert.Error(t, err)
assert.Nil(t, content)
}()
}
wg.Wait()
}
func TestErrors_VisitAll(t *testing.T) {
e := tpl.NewErrors(
map[string][]byte{
"foo": []byte("{{ code }}: {{ message }} {{ description }}"),
"bar": []byte("{{ code }}: {{ message }} {{ description }}"),
},
map[string]tpl.Annotator{
"200": {"ok", "all is ok"},
"400": {"Bad Request", "The server did not understand the request"},
},
)
visited := make(map[string]map[string]bool) // map[template]codes
assert.NoError(t, e.VisitAll(func(template, code string, content []byte) error {
if _, ok := visited[template]; !ok {
visited[template] = make(map[string]bool)
}
visited[template][code] = true
assert.NotNil(t, content)
return nil
}))
assert.Len(t, visited, 2)
assert.Len(t, visited["foo"], 2)
assert.True(t, visited["foo"]["200"])
assert.True(t, visited["foo"]["400"])
assert.Len(t, visited["bar"], 2)
assert.True(t, visited["bar"]["200"])
assert.True(t, visited["bar"]["400"])
}
func TestErrors_VisitAllWillReturnTheError(t *testing.T) {
e := tpl.NewErrors(
map[string][]byte{
"foo": []byte("{{ code }}: {{ message }} {{ description }}"),
},
map[string]tpl.Annotator{
"200": {"ok", "all is ok"},
},
)
assert.EqualError(t, e.VisitAll(func(template, code string, content []byte) error {
return errors.New("foo error")
}), "foo error")
}

47
internal/tpl/replace.go Normal file
View File

@ -0,0 +1,47 @@
package tpl
import "bytes"
type Replaces struct {
Code string
Message string
Description string
}
const (
tknCode byte = iota + 1
tknMessage
tknDescription
)
var tknSets = map[byte][][]byte{ //nolint:gochecknoglobals
tknCode: {[]byte("{{code}}"), []byte("{{ code }}")},
tknMessage: {[]byte("{{message}}"), []byte("{{ message }}")},
tknDescription: {[]byte("{{description}}"), []byte("{{ description }}")},
}
// Replace found tokens in the incoming slice with passed tokens.
func Replace(in []byte, re Replaces) []byte {
for tkn, set := range tknSets {
var replaceWith []byte
switch tkn {
case tknCode:
replaceWith = []byte(re.Code)
case tknMessage:
replaceWith = []byte(re.Message)
case tknDescription:
replaceWith = []byte(re.Description)
default:
panic("tpl: unsupported token")
}
if len(replaceWith) > 0 {
for i := 0; i < len(set); i++ {
in = bytes.ReplaceAll(in, set[i], replaceWith)
}
}
}
return in
}

View File

@ -0,0 +1,66 @@
package tpl_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tarampampam/error-pages/internal/tpl"
)
func ExampleReplace() {
var in = []byte("{{ code }}: {{message}} ({{ description }})")
fmt.Println(string(tpl.Replace(in, tpl.Replaces{
Code: "400",
Message: "Bad Request",
Description: "The server did not understand the request",
})))
// Output:
// 400: Bad Request (The server did not understand the request)
}
func TestReplace(t *testing.T) {
for name, tt := range map[string]struct {
giveIn []byte
giveRe tpl.Replaces
wantResult []byte
}{
"common": {
giveIn: []byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "),
giveRe: tpl.Replaces{
Code: "123",
Message: "message",
Description: "desc",
},
wantResult: []byte("-- 123 123 __ message desc "),
},
"alpha and underline in the code": {
giveIn: []byte("\t{{ code }}\t"),
giveRe: tpl.Replaces{
Code: " qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ ",
},
wantResult: []byte("\t qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ \t"),
},
} {
tt := tt
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.wantResult, tpl.Replace(tt.giveIn, tt.giveRe))
})
}
}
func BenchmarkReplace(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
tpl.Replace([]byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "), tpl.Replaces{
Code: "123",
Message: "message",
Description: "desc",
})
}
}

View File

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

View File

@ -0,0 +1,38 @@
package version
import (
"testing"
)
func TestVersion(t *testing.T) {
for give, want := range map[string]string{
// without changes
"vvv": "vvv",
"victory": "victory",
"voodoo": "voodoo",
"foo": "foo",
"0.0.0": "0.0.0",
"v": "v",
"V": "V",
// "v" prefix removal
"v0.0.0": "0.0.0",
"V0.0.0": "0.0.0",
"v1": "1",
"V1": "1",
// with spaces
" 0.0.0": "0.0.0",
"v0.0.0 ": "0.0.0",
" V0.0.0": "0.0.0",
"v1 ": "1",
" V1": "1",
"v ": "v",
} {
version = give
if v := Version(); v != want {
t.Errorf("want: %s, got: %s", want, v)
}
}
}