diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..0e9b4e1
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,26 @@
+# Docs:
+
+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
diff --git a/.dockerignore b/.dockerignore
index da0174a..4bf437a 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -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
diff --git a/.editorconfig b/.editorconfig
index dd3885c..fbdc476 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,3 +1,5 @@
+# EditorConfig docs:
+
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
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 13a93bd..6680ee3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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:
+ - 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:
+ - uses: gacts/github-slug@v1
+ id: slug
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v1 # Action page:
+ - uses: docker/setup-qemu-action@v1 # Action page:
- - name: Login to default Container Registry
- uses: docker/login-action@v1 # Action page:
+ - uses: docker/setup-buildx-action@v1 # Action page:
+
+ - uses: docker/login-action@v1 # Action page:
with:
username: ${{ secrets.DOCKER_LOGIN }}
password: ${{ secrets.DOCKER_PASSWORD }}
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v1 # Action page:
+ - uses: docker/login-action@v1 # Action page:
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:
+ 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
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 385c422..a9e2293 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -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:
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:
+ - uses: zricethezav/gitleaks-action@v1 # Action page:
- 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:
+ - name: Run linter
+ uses: golangci/golangci-lint-action@v2 # Action page:
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:
+ 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:
+ 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:
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:
+ - uses: docker/build-push-action@v2 # Action page:
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:
+ 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
diff --git a/.gitignore b/.gitignore
index a664320..c569fe6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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*.*
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..fce316e
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,96 @@
+# Documentation:
+
+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:
+ 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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7da3c1c..dbc794a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Dockerfile b/Dockerfile
index 20923db..f1911e7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,52 +1,76 @@
-# Image page:
-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:
+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:
-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:
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:
-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"]
diff --git a/Makefile b/Makefile
index ef28896..76395bf 100644
--- a/Makefile
+++ b/Makefile
@@ -3,27 +3,59 @@
# Makefile readme (en):
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 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
diff --git a/README.md b/README.md
index 23f75e3..5157a53 100644
--- a/README.md
+++ b/README.md
@@ -2,62 +2,190 @@
-# 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
+
+
+
-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].
+
+ Dive screenshot
-And open in your browser `http://127.0.0.1:8082/ghost/400.html`.
+
+
+
+
+
+
+
+## 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
+
+{{ code }}
+
+ {{ message }}: {{ description }}
+
+
+```
+
+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
+
+400
+
+ Bad Request: The server did not understand the request
+
+
+
+$ 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/
diff --git a/cmd/error-pages/main.go b/cmd/error-pages/main.go
new file mode 100644
index 0000000..9a48bc0
--- /dev/null
+++ b/cmd/error-pages/main.go
@@ -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:
+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
+}
diff --git a/config.json b/config.json
deleted file mode 100644
index 2045eae..0000000
--- a/config.json
+++ /dev/null
@@ -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"
- }
- ]
-}
diff --git a/docker-compose.yml b/docker-compose.yml
index 4575c36..8335286 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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:
+ app: &app-service
+ image: golang:1.17.1-buster # Image page:
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
+ 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:
+ environment:
+ GOLANGCI_LINT_CACHE: /tmp/golint #
+ volumes:
+ - .:/src:ro
+ - golint-cache:/tmp/golint:rw
+ working_dir: /src
+ command: /bin/true
diff --git a/docker/docker-entrypoint.d/100-setup-error-pages.sh b/docker/docker-entrypoint.d/100-setup-error-pages.sh
deleted file mode 100755
index 3f696a5..0000000
--- a/docker/docker-entrypoint.d/100-setup-error-pages.sh
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env sh
-set -e
-
-# on `docker restart` next directory keep existing:
-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
diff --git a/docker/nginx-server.conf b/docker/nginx-server.conf
deleted file mode 100644
index 0b7e11d..0000000
--- a/docker/nginx-server.conf
+++ /dev/null
@@ -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;
- }
-}
diff --git a/error-pages.yml b/error-pages.yml
new file mode 100644
index 0000000..cfca1ab
--- /dev/null
+++ b/error-pages.yml
@@ -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
diff --git a/generator/.gitignore b/generator/.gitignore
deleted file mode 100644
index b595b3a..0000000
--- a/generator/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-## Vendors
-/node_modules
-
-## Lock files (use yarn only)
-package-lock.json
-
-## Temp dirs & trash
-npm-debug.log
-yarn-error.log
diff --git a/generator/generator.js b/generator/generator.js
deleted file mode 100755
index 46cebed..0000000
--- a/generator/generator.js
+++ /dev/null
@@ -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 -d ')
- .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 = `
-
-
-
-
- Error pages list
-
-
-
-\n`;
-
- Object.keys(generated).forEach(function(templateName) {
- content += `Template name: ` + templateName + `
\n\n`;
- });
-
- content += `
-
-
-`;
-
- fs.writeFileSync(indexPageFilePath, content, {
- encoding: "utf8",
- flag: "w+",
- mode: 0o644
- });
- }
-} catch (err) {
- console.error(err);
-
- process.exit(1);
-}
diff --git a/generator/package.json b/generator/package.json
deleted file mode 100644
index 0bebd12..0000000
--- a/generator/package.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "error-pages",
- "repository": {
- "type": "git",
- "url": "git://github.com/tarampampam/error-pages.git"
- },
- "dependencies": {
- "yargs": "16.2"
- }
-}
diff --git a/generator/yarn.lock b/generator/yarn.lock
deleted file mode 100644
index 7274f01..0000000
--- a/generator/yarn.lock
+++ /dev/null
@@ -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"
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..00fb0a4
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..857f2a9
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/internal/breaker/os_signal.go b/internal/breaker/os_signal.go
new file mode 100644
index 0000000..41d532a
--- /dev/null
+++ b/internal/breaker/os_signal.go
@@ -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)
+}
diff --git a/internal/breaker/os_signal_test.go b/internal/breaker/os_signal_test.go
new file mode 100644
index 0000000..fc2fc29
--- /dev/null
+++ b/internal/breaker/os_signal_test.go
@@ -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)
+}
diff --git a/internal/checkers/health.go b/internal/checkers/health.go
new file mode 100644
index 0000000..a6da48f
--- /dev/null
+++ b/internal/checkers/health.go
@@ -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
+}
diff --git a/internal/checkers/health_test.go b/internal/checkers/health_test.go
new file mode 100644
index 0000000..a4f72b2
--- /dev/null
+++ b/internal/checkers/health_test.go
@@ -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")
+}
diff --git a/internal/checkers/live.go b/internal/checkers/live.go
new file mode 100644
index 0000000..10c666c
--- /dev/null
+++ b/internal/checkers/live.go
@@ -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 }
diff --git a/internal/checkers/live_test.go b/internal/checkers/live_test.go
new file mode 100644
index 0000000..413d89b
--- /dev/null
+++ b/internal/checkers/live_test.go
@@ -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())
+}
diff --git a/internal/cli/build/command.go b/internal/cli/build/command.go
new file mode 100644
index 0000000..98f613e
--- /dev/null
+++ b/internal/cli/build/command.go
@@ -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 ",
+ 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(`
+
+
+
+ Error pages list
+
+
+
+
+
+
+
+
Error pages index
+
+{{- range $template, $item := . -}}
+ Template name: {{ $template }}
+
+{{ end }}
+
+
+
+
+`)
+ 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
+}
diff --git a/internal/cli/build/command_test.go b/internal/cli/build/command_test.go
new file mode 100644
index 0000000..df9157a
--- /dev/null
+++ b/internal/cli/build/command_test.go
@@ -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")
+}
diff --git a/internal/cli/healthcheck/command.go b/internal/cli/healthcheck/command.go
new file mode 100644
index 0000000..283b9fa
--- /dev/null
+++ b/internal/cli/healthcheck/command.go
@@ -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
+}
diff --git a/internal/cli/healthcheck/command_test.go b/internal/cli/healthcheck/command_test.go
new file mode 100644
index 0000000..233aa90
--- /dev/null
+++ b/internal/cli/healthcheck/command_test.go
@@ -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)
+}
diff --git a/internal/cli/root.go b/internal/cli/root.go
new file mode 100644
index 0000000..c18b572
--- /dev/null
+++ b/internal/cli/root.go
@@ -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:
+ // -
+ // -
+ _ = 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
+}
diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go
new file mode 100644
index 0000000..e023fc6
--- /dev/null
+++ b/internal/cli/root_test.go
@@ -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)
+}
diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go
new file mode 100644
index 0000000..d38fc0a
--- /dev/null
+++ b/internal/cli/serve/command.go
@@ -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
+}
diff --git a/internal/cli/serve/command_test.go b/internal/cli/serve/command_test.go
new file mode 100644
index 0000000..2882055
--- /dev/null
+++ b/internal/cli/serve/command_test.go
@@ -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")
+}
diff --git a/internal/cli/serve/flags.go b/internal/cli/serve/flags.go
new file mode 100644
index 0000000..6994156
--- /dev/null
+++ b/internal/cli/serve/flags.go
@@ -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
+}
diff --git a/internal/cli/version/command.go b/internal/cli/version/command.go
new file mode 100644
index 0000000..e3b4b9d
--- /dev/null
+++ b/internal/cli/version/command.go
@@ -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
+ },
+ }
+}
diff --git a/internal/cli/version/command_test.go b/internal/cli/version/command_test.go
new file mode 100644
index 0000000..27db5bc
--- /dev/null
+++ b/internal/cli/version/command_test.go
@@ -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())
+}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..b9eb7fa
--- /dev/null
+++ b/internal/config/config.go
@@ -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)
+}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
new file mode 100644
index 0000000..48930b3
--- /dev/null
+++ b/internal/config/config_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/internal/config/testdata/broken.yml b/internal/config/testdata/broken.yml
new file mode 100644
index 0000000..d675fa4
--- /dev/null
+++ b/internal/config/testdata/broken.yml
@@ -0,0 +1 @@
+foo bar
diff --git a/internal/config/testdata/simple.yml b/internal/config/testdata/simple.yml
new file mode 100644
index 0000000..ce84547
--- /dev/null
+++ b/internal/config/testdata/simple.yml
@@ -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
diff --git a/internal/env/env.go b/internal/env/env.go
new file mode 100644
index 0000000..a3245fd
--- /dev/null
+++ b/internal/env/env.go
@@ -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)) }
diff --git a/internal/env/env_test.go b/internal/env/env_test.go
new file mode 100644
index 0000000..5200b7e
--- /dev/null
+++ b/internal/env/env_test.go
@@ -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)
+ })
+ }
+}
diff --git a/internal/http/common/handlers.go b/internal/http/common/handlers.go
new file mode 100644
index 0000000..4cae565
--- /dev/null
+++ b/internal/http/common/handlers.go
@@ -0,0 +1,35 @@
+package common
+
+import (
+ "strings"
+
+ "github.com/valyala/fasthttp"
+)
+
+const internalErrorPattern = `
+
+
+
+
+
+ Internal error occurred
+
+
+
+
+
+
{{ message }}
+
+
+`
+
+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))
+}
diff --git a/internal/http/common/handlers_test.go b/internal/http/common/handlers_test.go
new file mode 100644
index 0000000..56140ff
--- /dev/null
+++ b/internal/http/common/handlers_test.go
@@ -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")
+}
diff --git a/internal/http/common/middlewares.go b/internal/http/common/middlewares.go
new file mode 100644
index 0000000..8043889
--- /dev/null
+++ b/internal/http/common/middlewares.go
@@ -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)),
+ )
+ }
+}
diff --git a/internal/http/common/middlewares_test.go b/internal/http/common/middlewares_test.go
new file mode 100644
index 0000000..f65e6b8
--- /dev/null
+++ b/internal/http/common/middlewares_test.go
@@ -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")
+}
diff --git a/internal/http/handlers/errorpage/handler.go b/internal/http/handlers/errorpage/handler.go
new file mode 100644
index 0000000..3b07f13
--- /dev/null
+++ b/internal/http/handlers/errorpage/handler.go
@@ -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
+}
diff --git a/internal/http/handlers/errorpage/handler_test.go b/internal/http/handlers/errorpage/handler_test.go
new file mode 100644
index 0000000..5c974ff
--- /dev/null
+++ b/internal/http/handlers/errorpage/handler_test.go
@@ -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")
+}
diff --git a/internal/http/handlers/healthz/handler.go b/internal/http/handlers/healthz/handler.go
new file mode 100644
index 0000000..0e55fac
--- /dev/null
+++ b/internal/http/handlers/healthz/handler.go
@@ -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)
+ }
+}
diff --git a/internal/http/handlers/healthz/handler_test.go b/internal/http/handlers/healthz/handler_test.go
new file mode 100644
index 0000000..fbc25f2
--- /dev/null
+++ b/internal/http/handlers/healthz/handler_test.go
@@ -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")
+}
diff --git a/internal/http/handlers/version/handler.go b/internal/http/handlers/version/handler.go
new file mode 100644
index 0000000..7a8f376
--- /dev/null
+++ b/internal/http/handlers/version/handler.go
@@ -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)
+ }
+}
diff --git a/internal/http/handlers/version/handler_test.go b/internal/http/handlers/version/handler_test.go
new file mode 100644
index 0000000..72c102b
--- /dev/null
+++ b/internal/http/handlers/version/handler_test.go
@@ -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")
+}
diff --git a/internal/http/server.go b/internal/http/server.go
new file mode 100644
index 0000000..f3f945f
--- /dev/null
+++ b/internal/http/server.go
@@ -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:
+ 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:
+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()
+ }
+}
diff --git a/internal/http/server_test.go b/internal/http/server_test.go
new file mode 100644
index 0000000..66c2992
--- /dev/null
+++ b/internal/http/server_test.go
@@ -0,0 +1,7 @@
+package http
+
+import "testing"
+
+func TestNothing(t *testing.T) {
+ t.Skip("tests for this package have not been implemented yet")
+}
diff --git a/internal/logger/logger.go b/internal/logger/logger.go
new file mode 100644
index 0000000..b503de8
--- /dev/null
+++ b/internal/logger/logger.go
@@ -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()
+}
diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go
new file mode 100644
index 0000000..b6421a9
--- /dev/null
+++ b/internal/logger/logger_test.go
@@ -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])
+}
diff --git a/internal/tpl/errors.go b/internal/tpl/errors.go
new file mode 100644
index 0000000..0413332
--- /dev/null
+++ b/internal/tpl/errors.go
@@ -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
+}
diff --git a/internal/tpl/errors_test.go b/internal/tpl/errors_test.go
new file mode 100644
index 0000000..681d58f
--- /dev/null
+++ b/internal/tpl/errors_test.go
@@ -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")
+}
diff --git a/internal/tpl/replace.go b/internal/tpl/replace.go
new file mode 100644
index 0000000..704b707
--- /dev/null
+++ b/internal/tpl/replace.go
@@ -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
+}
diff --git a/internal/tpl/replace_test.go b/internal/tpl/replace_test.go
new file mode 100644
index 0000000..e25bc05
--- /dev/null
+++ b/internal/tpl/replace_test.go
@@ -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",
+ })
+ }
+}
diff --git a/internal/version/version.go b/internal/version/version.go
new file mode 100644
index 0000000..e5db922
--- /dev/null
+++ b/internal/version/version.go
@@ -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
+}
diff --git a/internal/version/version_test.go b/internal/version/version_test.go
new file mode 100644
index 0000000..bed3e5b
--- /dev/null
+++ b/internal/version/version_test.go
@@ -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)
+ }
+ }
+}