mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
v2: App rewritten in Go (#25)
This commit is contained in:
parent
ce98410e51
commit
29f024ebcc
26
.codecov.yml
Normal file
26
.codecov.yml
Normal file
@ -0,0 +1,26 @@
|
||||
# Docs: <https://docs.codecov.io/docs/commit-status>
|
||||
|
||||
coverage:
|
||||
# coverage lower than 50 is red, higher than 90 green
|
||||
range: 30..80
|
||||
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
# Choose a minimum coverage ratio that the commit must meet to be considered a success.
|
||||
#
|
||||
# `auto` will use the coverage from the base commit (pull request base or parent commit) coverage to compare
|
||||
# against.
|
||||
target: auto
|
||||
|
||||
# Allow the coverage to drop by X%, and posting a success status.
|
||||
threshold: 5%
|
||||
|
||||
# Resulting status will pass no matter what the coverage is or what other settings are specified.
|
||||
informational: true
|
||||
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 5%
|
||||
informational: true
|
@ -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
|
||||
|
@ -1,3 +1,5 @@
|
||||
# EditorConfig docs: <https://editorconfig.org/>
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
@ -11,5 +13,5 @@ trim_trailing_whitespace = true
|
||||
[*.{yml, yaml, sh, conf}]
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
[{Makefile, go.mod, *.go}]
|
||||
indent_style = tab
|
||||
|
144
.github/workflows/release.yml
vendored
144
.github/workflows/release.yml
vendored
@ -5,100 +5,96 @@ on:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
demo:
|
||||
name: Update demonstration, hosted on github pages
|
||||
build:
|
||||
name: Build for ${{ matrix.os }} (${{ matrix.arch }})
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [linux, darwin] # linux, freebsd, darwin, windows
|
||||
arch: [amd64] # amd64, 386
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with: {go-version: 1.17.1}
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v1 # Action page: <https://github.com/actions/setup-node>
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- name: Generate builder values
|
||||
id: values
|
||||
run: echo "::set-output name=binary-name::error-pages-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
CGO_ENABLED: 0
|
||||
LDFLAGS: -s -w -X github.com/tarampampam/error-pages/internal/version.version=${{ steps.slug.outputs.version }}
|
||||
run: go build -trimpath -ldflags "$LDFLAGS" -o "./${{ steps.values.outputs.binary-name }}" ./cmd/error-pages/
|
||||
|
||||
- name: Upload binary file to release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
node-version: 15
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: generator
|
||||
run: yarn install
|
||||
|
||||
- name: Generate pages
|
||||
run: ./generator/generator.js -i -c ./config.json -o ./out
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: content
|
||||
path: out/
|
||||
retention-days: 1
|
||||
|
||||
- name: Switch to github pages branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: gh-pages
|
||||
|
||||
- name: Download artifact
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: content
|
||||
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name "$GITHUB_ACTOR"
|
||||
git config --global user.email 'actions@github.com'
|
||||
git remote add github "https://$GITHUB_ACTOR:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git"
|
||||
|
||||
- name: Stage changes
|
||||
run: git add .
|
||||
|
||||
- name: Commit changes
|
||||
run: git commit --allow-empty -m "Deploying ${GITHUB_SHA} to Github Pages"
|
||||
|
||||
- name: Push changes
|
||||
run: git push github --force
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ steps.values.outputs.binary-name }}
|
||||
asset_name: ${{ steps.values.outputs.binary-name }}
|
||||
tag: ${{ github.ref }}
|
||||
|
||||
docker-image:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1 # Action page: <https://github.com/docker/setup-qemu-action>
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1 # Action page: <https://github.com/docker/setup-buildx-action>
|
||||
- uses: docker/setup-qemu-action@v1 # Action page: <https://github.com/docker/setup-qemu-action>
|
||||
|
||||
- name: Login to default Container Registry
|
||||
uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
|
||||
- uses: docker/setup-buildx-action@v1 # Action page: <https://github.com/docker/setup-buildx-action>
|
||||
|
||||
- uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_LOGIN }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
|
||||
- uses: docker/login-action@v1 # Action page: <https://github.com/docker/login-action>
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_PASSWORD }}
|
||||
|
||||
- name: Generate builder values
|
||||
id: values
|
||||
run: echo "::set-output name=version::${GITHUB_REF##*/[vV]}" # `/refs/tags/v1.2.3` -> `1.2.3`
|
||||
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action>
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
build-args: "APP_VERSION=${{ steps.slug.outputs.version }}"
|
||||
tags: |
|
||||
tarampampam/error-pages:${{ steps.slug.outputs.version }}
|
||||
tarampampam/error-pages:latest
|
||||
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
|
||||
ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:latest
|
||||
|
||||
- name: Build image
|
||||
demo:
|
||||
name: Update the demonstration
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [docker-image]
|
||||
steps:
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- name: Take rendered templates from the built docker image
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform "linux/amd64,linux/arm64/v8,linux/arm/v6,linux/arm/v7" \
|
||||
--tag "tarampampam/error-pages:${{ steps.values.outputs.version }}" \
|
||||
--tag "tarampampam/error-pages:latest" \
|
||||
--tag "ghcr.io/${{ github.actor }}/error-pages:${{ steps.values.outputs.version }}" \
|
||||
--tag "ghcr.io/${{ github.actor }}/error-pages:latest" \
|
||||
--file ./Dockerfile \
|
||||
--push \
|
||||
.
|
||||
docker create --name img ghcr.io/${{ github.actor }}/${{ github.event.repository.name }}:${{ steps.slug.outputs.version }}
|
||||
docker cp img:/opt/html ./out
|
||||
docker rm -f img
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./out
|
||||
|
213
.github/workflows/tests.yml
vendored
213
.github/workflows/tests.yml
vendored
@ -2,54 +2,130 @@ name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags-ignore:
|
||||
- '**'
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
branches: [master, main]
|
||||
tags-ignore: ['**']
|
||||
paths-ignore: ['**.md']
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
paths-ignore: ['**.md']
|
||||
|
||||
jobs: # Docs: <https://git.io/JvxXE>
|
||||
gitleaks:
|
||||
name: Gitleaks
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/checkout@v2
|
||||
with: {fetch-depth: 0}
|
||||
|
||||
- name: Check for GitLeaks
|
||||
uses: zricethezav/gitleaks-action@v1.5.0 # Action page: <https://github.com/zricethezav/gitleaks-action>
|
||||
- uses: zricethezav/gitleaks-action@v1 # Action page: <https://github.com/zricethezav/gitleaks-action>
|
||||
|
||||
generate:
|
||||
name: Try to run generator
|
||||
golangci-lint:
|
||||
name: Golang-CI (lint)
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v1 # Action page: <https://github.com/actions/setup-node>
|
||||
- name: Run linter
|
||||
uses: golangci/golangci-lint-action@v2 # Action page: <https://github.com/golangci/golangci-lint-action>
|
||||
with:
|
||||
node-version: 15
|
||||
version: v1.42 # without patch version
|
||||
only-new-issues: false # show only new issues if it's a pull request
|
||||
|
||||
- uses: actions/cache@v2
|
||||
go-test:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with: {go-version: 1.17}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with: {fetch-depth: 2} # Fixes codecov error 'Issue detecting commit SHA'
|
||||
|
||||
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
|
||||
uses: actions/cache@v2
|
||||
id: go-cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: generator
|
||||
run: yarn install
|
||||
- if: steps.go-cache.outputs.cache-hit != 'true'
|
||||
run: go mod download
|
||||
|
||||
- name: Run Unit tests
|
||||
run: go test -race -covermode=atomic -coverprofile /tmp/coverage.txt ./...
|
||||
|
||||
- uses: codecov/codecov-action@v2 # https://github.com/codecov/codecov-action
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: /tmp/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build:
|
||||
name: Build for ${{ matrix.os }} (${{ matrix.arch }})
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [linux, darwin] # linux, freebsd, darwin, windows
|
||||
arch: [amd64] # amd64, 386
|
||||
needs: [golangci-lint, go-test]
|
||||
steps:
|
||||
- uses: actions/setup-go@v2
|
||||
with: {go-version: 1.17}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-
|
||||
|
||||
- run: go mod download
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
CGO_ENABLED: 0
|
||||
LDFLAGS: -s -w -X github.com/tarampampam/error-pages/internal/version.version=${{ steps.slug.outputs.branch-name-slug }}@${{ steps.slug.outputs.commit-hash-short }}
|
||||
run: go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/
|
||||
|
||||
- name: Try to execute
|
||||
if: matrix.os == 'linux'
|
||||
run: ./error-pages version && ./error-pages -h
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: error-pages-${{ matrix.os }}-${{ matrix.arch }}
|
||||
path: error-pages
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
generate:
|
||||
name: Run templates generator
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [build]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: error-pages-linux-amd64
|
||||
path: .artifact
|
||||
|
||||
- name: Prepare binary file to run
|
||||
working-directory: .artifact
|
||||
run: mv ./error-pages ./../error-pages && chmod +x ./../error-pages
|
||||
|
||||
- name: Run generator
|
||||
run: ./generator/generator.js -i -c ./config.json -o ./out
|
||||
run: ./error-pages build ./out --verbose --index
|
||||
|
||||
- name: Test file creation
|
||||
- name: Test files creation
|
||||
run: |
|
||||
test -f ./out/index.html
|
||||
test -f ./out/ghost/404.html
|
||||
@ -59,31 +135,76 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
test -f ./out/noise/404.html
|
||||
test -f ./out/hacker-terminal/404.html
|
||||
|
||||
docker-build:
|
||||
docker-image:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [golangci-lint, go-test]
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Build docker image
|
||||
run: docker build -f ./Dockerfile --tag image:local .
|
||||
- uses: gacts/github-slug@v1
|
||||
id: slug
|
||||
|
||||
- name: Scan image
|
||||
uses: anchore/scan-action@v2 # action page: <https://github.com/anchore/scan-action>
|
||||
- uses: docker/build-push-action@v2 # Action page: <https://github.com/docker/build-push-action>
|
||||
with:
|
||||
image: image:local
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: false
|
||||
build-args: "APP_VERSION=${{ steps.slug.outputs.branch-name-slug }}@${{ steps.slug.outputs.commit-hash-short }}"
|
||||
tags: app:ci
|
||||
|
||||
- run: docker save app:ci > ./docker-image.tar
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: docker-image
|
||||
path: ./docker-image.tar
|
||||
retention-days: 1
|
||||
|
||||
scan-docker-image:
|
||||
name: Scan the docker image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [docker-image]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docker-image
|
||||
path: .artifact
|
||||
|
||||
- working-directory: .artifact
|
||||
run: docker load < docker-image.tar
|
||||
|
||||
- uses: anchore/scan-action@v3 # action page: <https://github.com/anchore/scan-action>
|
||||
with:
|
||||
image: app:ci
|
||||
fail-build: true
|
||||
severity-cutoff: medium # negligible, low, medium, high or critical
|
||||
severity-cutoff: low # negligible, low, medium, high or critical
|
||||
|
||||
- name: Run docker image
|
||||
run: docker run --rm -d -p "8080:8080/tcp" -e "TEMPLATE_NAME=ghost" image:local
|
||||
poke-docker-image:
|
||||
name: Run the docker image
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [docker-image]
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docker-image
|
||||
path: .artifact
|
||||
|
||||
- name: Pause
|
||||
run: sleep 2
|
||||
- working-directory: .artifact
|
||||
run: docker load < docker-image.tar
|
||||
|
||||
- name: Verify 500.html error file exists in root
|
||||
run: curl -sS --fail "http://127.0.0.1:8080/500.html"
|
||||
- name: Run container with the app
|
||||
run: docker run --rm -d -p "8080:8080/tcp" --name app app:ci
|
||||
|
||||
- name: Verify root request HTTP code
|
||||
run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/) -eq 404
|
||||
- name: Wait for container "healthy" state
|
||||
run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done
|
||||
|
||||
- run: curl --fail http://127.0.0.1:8080/500.html
|
||||
- run: curl --fail http://127.0.0.1:8080/400.html
|
||||
- run: curl --fail http://127.0.0.1:8080/health/live
|
||||
- run: test $(curl --write-out %{http_code} --silent --output /dev/null http://127.0.0.1:8080/) -eq 404
|
||||
|
||||
- name: Stop the container
|
||||
if: always()
|
||||
run: docker kill app
|
||||
|
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,9 +1,16 @@
|
||||
## IDEs
|
||||
/.vscode
|
||||
/.idea
|
||||
/.vscode
|
||||
|
||||
## Dist
|
||||
/out
|
||||
## Binaries
|
||||
error-pages
|
||||
|
||||
## Temp dirs & trash
|
||||
/temp
|
||||
/tmp
|
||||
*.env
|
||||
.DS_Store
|
||||
*.cache
|
||||
*.out
|
||||
/out
|
||||
/cover*.*
|
||||
|
96
.golangci.yml
Normal file
96
.golangci.yml
Normal file
@ -0,0 +1,96 @@
|
||||
# Documentation: <https://github.com/golangci/golangci-lint#config-file>
|
||||
|
||||
run:
|
||||
timeout: 1m
|
||||
skip-dirs:
|
||||
- .github
|
||||
- .git
|
||||
modules-download-mode: readonly
|
||||
allow-parallel-runners: true
|
||||
|
||||
output:
|
||||
format: colored-line-number # colored-line-number|line-number|json|tab|checkstyle|code-climate
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
godot:
|
||||
scope: declarations
|
||||
capital: true
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 3
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 120
|
||||
prealloc:
|
||||
simple: true
|
||||
range-loops: true
|
||||
for-loops: true
|
||||
nolintlint:
|
||||
allow-leading-space: false
|
||||
require-specific: true
|
||||
|
||||
linters: # All available linters list: <https://golangci-lint.run/usage/linters/>
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
|
||||
- bodyclose # Checks whether HTTP response body is closed successfully
|
||||
- deadcode # Finds unused code
|
||||
- depguard # Go linter that checks if package imports are in a list of acceptable packages
|
||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
|
||||
- dupl # Tool for code clone detection
|
||||
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
|
||||
- errorlint # find code that will cause problems with the error wrapping scheme introduced in Go 1.13
|
||||
- exhaustive # check exhaustiveness of enum switch statements
|
||||
- exportloopref # checks for pointers to enclosing loop variables
|
||||
- funlen # Tool for detection of long functions
|
||||
- gochecknoglobals # Checks that no globals are present in Go code
|
||||
- gochecknoinits # Checks that no init functions are present in Go code
|
||||
- gocognit # Computes and checks the cognitive complexity of functions
|
||||
- goconst # Finds repeated strings that could be replaced by a constant
|
||||
- gocritic # The most opinionated Go source code linter
|
||||
- gocyclo # Computes and checks the cyclomatic complexity of functions
|
||||
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
|
||||
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
|
||||
- gomnd # An analyzer to detect magic numbers
|
||||
- goprintffuncname # Checks that printf-like functions are named with `f` at the end
|
||||
- gosec # Inspects source code for security problems
|
||||
- gosimple # Linter for Go source code that specializes in simplifying a code
|
||||
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
|
||||
- ineffassign # Detects when assignments to existing variables are not used
|
||||
- lll # Reports long lines
|
||||
- misspell # Finds commonly misspelled English words in comments
|
||||
- nakedret # Finds naked returns in functions greater than a specified function length
|
||||
- nestif # Reports deeply nested if statements
|
||||
- nlreturn # checks for a new line before return and branch statements to increase code clarity
|
||||
- noctx # finds sending http request without context.Context
|
||||
- nolintlint # Reports ill-formed or insufficient nolint directives
|
||||
- prealloc # Finds slice declarations that could potentially be preallocated
|
||||
- rowserrcheck # Checks whether Err of rows is checked successfully
|
||||
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
|
||||
- structcheck # Finds unused struct fields
|
||||
- stylecheck # Stylecheck is a replacement for golint
|
||||
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
|
||||
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
|
||||
- unconvert # Remove unnecessary type conversions
|
||||
- unparam # Reports unused function parameters
|
||||
- unused # Checks Go code for unused constants, variables, functions and types
|
||||
- varcheck # Finds unused global variables and constants
|
||||
- whitespace # Tool for detection of leading and trailing whitespace
|
||||
- wsl # Whitespace Linter - Forces you to use empty lines!
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- funlen
|
||||
- scopelint
|
||||
- gocognit
|
||||
- noctx
|
@ -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
|
||||
|
80
Dockerfile
80
Dockerfile
@ -1,52 +1,76 @@
|
||||
# Image page: <https://hub.docker.com/_/node>
|
||||
FROM node:15.14-alpine as builder
|
||||
# syntax=docker/dockerfile:1.2
|
||||
|
||||
# copy required sources into builder image
|
||||
COPY ./generator /src/generator
|
||||
COPY ./config.json /src
|
||||
COPY ./templates /src/templates
|
||||
COPY ./docker /src/docker
|
||||
# Image page: <https://hub.docker.com/_/golang>
|
||||
FROM golang:1.17.1-alpine as builder
|
||||
|
||||
# install generator dependencies
|
||||
WORKDIR /src/generator
|
||||
RUN yarn install --frozen-lockfile --no-progress --non-interactive
|
||||
# can be passed with any prefix (like `v1.2.3@GITHASH`), e.g.: `docker build --build-arg "APP_VERSION=v1.2.3@GITHASH" .`
|
||||
ARG APP_VERSION="undefined@docker"
|
||||
|
||||
# run generator
|
||||
WORKDIR /src
|
||||
RUN ./generator/generator.js -c ./config.json -o ./out
|
||||
|
||||
COPY . .
|
||||
|
||||
# arguments to pass on each go tool link invocation
|
||||
ENV LDFLAGS="-s -w -X github.com/tarampampam/error-pages/internal/version.version=$APP_VERSION"
|
||||
|
||||
RUN set -x \
|
||||
&& go version \
|
||||
&& CGO_ENABLED=0 go build -trimpath -ldflags "$LDFLAGS" -o ./error-pages ./cmd/error-pages/ \
|
||||
&& ./error-pages version \
|
||||
&& ./error-pages -h
|
||||
|
||||
WORKDIR /tmp/rootfs
|
||||
|
||||
# prepare rootfs for runtime
|
||||
RUN mkdir /tmp/rootfs
|
||||
WORKDIR /tmp/rootfs
|
||||
RUN set -x \
|
||||
&& mkdir -p \
|
||||
./docker-entrypoint.d \
|
||||
./etc/nginx/conf.d \
|
||||
./opt \
|
||||
&& mv /src/out ./opt/html \
|
||||
&& echo -e "User-agent: *\nDisallow: /\n" > ./opt/html/robots.txt \
|
||||
&& touch ./opt/html/favicon.ico \
|
||||
&& mv /src/docker/docker-entrypoint.d/* ./docker-entrypoint.d \
|
||||
&& mv /src/docker/nginx-server.conf ./etc/nginx/conf.d/default.conf
|
||||
./etc \
|
||||
./bin \
|
||||
./opt/html \
|
||||
&& echo 'appuser:x:10001:10001::/nonexistent:/sbin/nologin' > ./etc/passwd \
|
||||
&& echo 'appuser:x:10001:' > ./etc/group \
|
||||
&& mv /src/error-pages ./bin/error-pages \
|
||||
&& mv /src/templates ./opt/templates \
|
||||
&& mv /src/error-pages.yml ./opt/error-pages.yml
|
||||
|
||||
# Image page: <https://hub.docker.com/_/nginx>
|
||||
FROM nginx:1.21.1-alpine as runtime
|
||||
WORKDIR /tmp/rootfs/opt
|
||||
|
||||
# generate static error pages (for usage inside another docker images, for example)
|
||||
RUN set -x \
|
||||
&& ./../bin/error-pages --config-file ./error-pages.yml build ./html --verbose --index \
|
||||
&& ls -l ./html
|
||||
|
||||
# use empty filesystem
|
||||
FROM scratch as runtime
|
||||
|
||||
ARG APP_VERSION="undefined@docker"
|
||||
|
||||
LABEL \
|
||||
# Docs: <https://github.com/opencontainers/image-spec/blob/master/annotations.md>
|
||||
org.opencontainers.image.title="error-pages" \
|
||||
org.opencontainers.image.description="Static server error pages in docker image" \
|
||||
org.opencontainers.image.description="Static server error pages in the docker image" \
|
||||
org.opencontainers.image.url="https://github.com/tarampampam/error-pages" \
|
||||
org.opencontainers.image.source="https://github.com/tarampampam/error-pages" \
|
||||
org.opencontainers.image.vendor="tarampampam" \
|
||||
org.opencontainers.version="$APP_VERSION" \
|
||||
org.opencontainers.image.licenses="MIT"
|
||||
|
||||
# Import from builder
|
||||
COPY --from=builder /tmp/rootfs /
|
||||
|
||||
# Use an unprivileged user
|
||||
USER appuser:appuser
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
ENV LISTEN_PORT="8080" \
|
||||
TEMPLATE_NAME="ghost"
|
||||
|
||||
# Docs: <https://docs.docker.com/engine/reference/builder/#healthcheck>
|
||||
HEALTHCHECK --interval=15s --timeout=2s --retries=2 --start-period=2s CMD [ \
|
||||
"wget", "--spider", "-q", "http://127.0.0.1:8080/health/live" \
|
||||
HEALTHCHECK --interval=7s --timeout=2s CMD [ \
|
||||
"/bin/error-pages", "healthcheck", "--log-json" \
|
||||
]
|
||||
|
||||
RUN chown -R nginx:nginx /opt/html
|
||||
ENTRYPOINT ["/bin/error-pages"]
|
||||
|
||||
CMD ["serve", "--log-json"]
|
||||
|
58
Makefile
58
Makefile
@ -3,27 +3,59 @@
|
||||
# Makefile readme (en): <https://www.gnu.org/software/make/manual/html_node/index.html#SEC_Contents>
|
||||
|
||||
SHELL = /bin/sh
|
||||
LDFLAGS = "-s -w -X github.com/tarampampam/error-pages/internal/version.version=$(shell git rev-parse HEAD)"
|
||||
|
||||
DC_RUN_ARGS = --rm --user "$(shell id -u):$(shell id -g)"
|
||||
APP_NAME = $(notdir $(CURDIR))
|
||||
|
||||
.PHONY : help install gen preview
|
||||
.PHONY : help \
|
||||
image dive build fmt lint gotest test shell \
|
||||
up down restart \
|
||||
clean
|
||||
.DEFAULT_GOAL : help
|
||||
.SILENT : lint gotest
|
||||
|
||||
# This will output the help for each task. thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
help: ## Show this help
|
||||
@printf "\033[33m%s:\033[0m\n" 'Available commands'
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[32m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[32m%-11s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
install: ## Install all dependencies
|
||||
docker-compose run $(DC_RUN_ARGS) -w "/src/generator" node yarn install --frozen-lockfile --no-progress --non-interactive
|
||||
|
||||
gen: ## Generate error pages
|
||||
docker-compose run $(DC_RUN_ARGS) node nodejs ./generator/generator.js -i -c ./config.json -o ./out
|
||||
|
||||
preview: ## Build docker image and start preview
|
||||
image: ## Build docker image with app
|
||||
docker build -f ./Dockerfile -t $(APP_NAME):local .
|
||||
@printf "\n \e[30;42m %s \033[0m\n\n" 'Now open in your favorite browser <http://127.0.0.1:8081> and press CTRL+C for stopping'
|
||||
docker run --rm -i -p 8081:8080 -e "TEMPLATE_NAME=random" $(APP_NAME):local
|
||||
docker run --rm $(APP_NAME):local version
|
||||
@printf "\n \e[30;42m %s \033[0m\n\n" 'Now you can use image like `docker run --rm -p "8080:8080/tcp" $(APP_NAME):local ...`';
|
||||
|
||||
shell: ## Start shell into container with node
|
||||
docker-compose run $(DC_RUN_ARGS) node sh
|
||||
dive: image ## Explore the docker image
|
||||
docker run --rm -it -v "/var/run/docker.sock:/var/run/docker.sock:ro" wagoodman/dive:latest $(APP_NAME):local
|
||||
|
||||
build: ## Build app binary file
|
||||
docker-compose run $(DC_RUN_ARGS) -e "CGO_ENABLED=0" --no-deps app go build -trimpath -ldflags $(LDFLAGS) -o ./error-pages ./cmd/error-pages/
|
||||
|
||||
fmt: ## Run source code formatter tools
|
||||
docker-compose run $(DC_RUN_ARGS) -e "GO111MODULE=off" --no-deps app sh -c 'go get golang.org/x/tools/cmd/goimports && $$GOPATH/bin/goimports -d -w .'
|
||||
docker-compose run $(DC_RUN_ARGS) --no-deps app gofmt -s -w -d .
|
||||
docker-compose run $(DC_RUN_ARGS) --no-deps app go mod tidy
|
||||
|
||||
lint: ## Run app linters
|
||||
docker-compose run --rm --no-deps golint golangci-lint run
|
||||
|
||||
gotest: ## Run app tests
|
||||
docker-compose run $(DC_RUN_ARGS) --no-deps app go test -v -race -timeout 10s ./...
|
||||
|
||||
test: lint gotest ## Run app tests and linters
|
||||
|
||||
shell: ## Start shell into container with golang
|
||||
docker-compose run $(DC_RUN_ARGS) app bash
|
||||
|
||||
up: ## Create and start containers
|
||||
docker-compose up --detach web
|
||||
@printf "\n \e[30;42m %s \033[0m\n\n" 'Navigate your browser to ⇒ http://127.0.0.1:8080';
|
||||
|
||||
down: ## Stop all services
|
||||
docker-compose down -t 5
|
||||
|
||||
restart: down up ## Restart all containers
|
||||
|
||||
clean: ## Make clean
|
||||
docker-compose down -v -t 1
|
||||
-docker rmi $(APP_NAME):local -f
|
||||
|
286
README.md
286
README.md
@ -2,62 +2,190 @@
|
||||
<img src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png" width="94" alt="" />
|
||||
</p>
|
||||
|
||||
# HTTP's error pages in Docker image
|
||||
# HTTP's error pages
|
||||
|
||||
[![Build Status][badge_build_status]][link_build_status]
|
||||
[![Release version][badge_release_version]][link_releases]
|
||||
![Project language][badge_language]
|
||||
[![Build Status][badge_build]][link_build]
|
||||
[![Release Status][badge_release]][link_build]
|
||||
[![Coverage][badge_coverage]][link_coverage]
|
||||
[![Image size][badge_size_latest]][link_docker_hub]
|
||||
[![Docker Pulls][badge_docker_pulls]][link_docker_hub]
|
||||
[![License][badge_license]][link_license]
|
||||
|
||||
This repository contains:
|
||||
One day you may want to replace the standard error pages of your HTTP server with something more original and pretty. That's what this repository was created for :) It contains:
|
||||
|
||||
- A very simple [generator](generator/generator.js) _(`nodejs`)_ for HTTP error pages _(like `404: Not found`)_ with different templates supports
|
||||
- Dockerfile for docker image ([docker hub][link_docker_hub], [ghcr.io][link_ghcr]) with generated pages and `nginx` as a web server
|
||||
- Simple error pages generator, written on Go
|
||||
- Single-page error page templates with different designs (located in the [templates](templates) directory)
|
||||
- Fast and lightweight HTTP server (written on Go also, with the [FastHTTP][fasthttp] under the hood)
|
||||
- Already generated error pages (sources can be [found here][preview-sources], the **demonstration** is always accessible [here][preview-demo])
|
||||
- Lightweight docker image _(~3.5Mb compressed size)_ with all the things described above
|
||||
|
||||
**Can be used for [Traefik error pages customization](https://docs.traefik.io/middlewares/errorpages/)**.
|
||||
Also, this project can be used for the [**Traefik** error pages customization](https://doc.traefik.io/traefik/middlewares/http/errorpages/).
|
||||
|
||||
## Demo
|
||||
<p align="center">
|
||||
<img src="https://hsto.org/webt/bc/bt/9i/bcbt9i3jyvozequr1e4maz7i2q8.png" alt="" />
|
||||
</p>
|
||||
|
||||
Generated pages (from the latest release) always **[accessible here][link_gh_pages]** _(live preview)_.
|
||||
## Installing
|
||||
|
||||
## Templates
|
||||
|
||||
Name | Preview
|
||||
:---------------: | :-----:
|
||||
`ghost` | [![ghost](https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif)](https://tarampampam.github.io/error-pages/ghost/404.html)
|
||||
`l7-light` | [![l7-light](https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png)](https://tarampampam.github.io/error-pages/l7-light/404.html)
|
||||
`l7-dark` | [![l7-dark](https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png)](https://tarampampam.github.io/error-pages/l7-dark/404.html)
|
||||
`shuffle` | [![shuffle](https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif)](https://tarampampam.github.io/error-pages/shuffle/404.html)
|
||||
`noise` | [![noise](https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif)](https://tarampampam.github.io/error-pages/noise/404.html)
|
||||
`hacker-terminal` | [![hacker-terminal](https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif)](https://tarampampam.github.io/error-pages/hacker-terminal/404.html)
|
||||
|
||||
> Note: `noise` template highly uses the CPU, be careful
|
||||
|
||||
## Usage
|
||||
|
||||
Generated error pages in our [docker image][link_docker_hub] permanently located in directory `/opt/html/%TEMPLATE_NAME%`. `nginx` in a container listen for `8080` (`http`) port.
|
||||
|
||||
#### Supported environment variables
|
||||
|
||||
Name | Description
|
||||
--------------- | -----------
|
||||
`TEMPLATE_NAME` | (`ghost` by default) "default" pages template _(allows to use error pages without passing theme name in URL - `http://127.0.0.1/500.html` instead `http://127.0.0.1/ghost/500.html`)_
|
||||
|
||||
Also, you can use a special template name `random` - in this case template will be selected randomly.
|
||||
|
||||
### Ready docker image
|
||||
Download the latest binary file for your os/arch from the [releases page][link_releases] or use our docker image:
|
||||
|
||||
[![image stats](https://dockeri.co/image/tarampampam/error-pages)][link_docker_hub]
|
||||
|
||||
Execute in your shell:
|
||||
Registry | Image
|
||||
-------------------------------------- | -----
|
||||
[Docker Hub][link_docker_hub] | `tarampampam/error-pages`
|
||||
[GitHub Container Registry][link_ghcr] | `ghcr.io/tarampampam/error-pages`
|
||||
|
||||
> Using the `latest` tag for the docker image is highly discouraged because of possible backward-incompatible changes during **major** upgrades. Please, use tags in `X.Y.Z` format
|
||||
|
||||
To watch the docker image content you can use the [dive](https://github.com/wagoodman/dive):
|
||||
|
||||
```bash
|
||||
$ docker run --rm -p "8082:8080" tarampampam/error-pages:X.X.X
|
||||
$ docker run --rm -it \
|
||||
-v "/var/run/docker.sock:/var/run/docker.sock:ro" \
|
||||
wagoodman/dive:latest \
|
||||
tarampampam/error-pages:latest
|
||||
```
|
||||
|
||||
> Important notice: do **not** use the `latest` image tag _(this is bad practice)_. Use versioned tag (like `1.2.3`) instead. Docker hub mirror located [here (ghcr.io)][link_ghcr].
|
||||
<details>
|
||||
<summary>Dive screenshot</summary>
|
||||
|
||||
And open in your browser `http://127.0.0.1:8082/ghost/400.html`.
|
||||
<p align="center">
|
||||
<img src="https://hsto.org/webt/mi/ak/uf/miakufsh2ibxtsa1nomfhyqombi.png" alt="" />
|
||||
</p>
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
All of the examples below will use a docker image with the application, but you can also use a binary. By the way, our docker image uses the **unleveled user** by default and **distroless**.
|
||||
|
||||
### HTTP server
|
||||
|
||||
As mentioned above - our application can be run as an HTTP server. It only needs to specify the path to the configuration file (it does not need statically generated error pages). The server uses [FastHTTP][fasthttp] and stores all necessary data in memory - so it does not use the file system and very fast. Oh yes, the image with the app also contains a configured **healthcheck** and **logs in JSON** format :)
|
||||
|
||||
For the HTTP server running execute in your terminal:
|
||||
|
||||
```bash
|
||||
$ docker run --rm \
|
||||
-p "8080:8080/tcp" \
|
||||
-e "TEMPLATE_NAME=random" \
|
||||
tarampampam/error-pages
|
||||
```
|
||||
|
||||
And open [`http://127.0.0.1:8080/404.html`](http://127.0.0.1:8080/404.html) in your favorite browser. Error pages are accessible by the following URLs: `http://127.0.0.1:8080/{page_code}.html`.
|
||||
|
||||
Environment variable `TEMPLATE_NAME` should be used for the theme switching (supported templates are described below).
|
||||
|
||||
> **Cheat**: you can use `random` (to use the randomized theme on server start) or `i-said-random` (to use the randomized template on **each request**)
|
||||
|
||||
To see the help run the following command:
|
||||
|
||||
```bash
|
||||
$ docker run --rm tarampampam/error-pages serve --help
|
||||
```
|
||||
|
||||
### Generator
|
||||
|
||||
Create a config file (`error-pages.yml`) with the following content:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
- path: ./foo.html # Template name "foo" (same as file name),
|
||||
# content located in the file "foo.html"
|
||||
- name: bar # Template name "bar", its content is described below:
|
||||
content: "Error {{ code }}: {{ message }} ({{ description }})"
|
||||
|
||||
pages:
|
||||
400:
|
||||
message: Bad Request
|
||||
description: The server did not understand the request
|
||||
|
||||
401:
|
||||
message: Unauthorized
|
||||
description: The requested page needs a username and a password
|
||||
```
|
||||
|
||||
Template file `foo.html`:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<title>{{ code }}</title>
|
||||
<body>
|
||||
<h1>{{ message }}: {{ description }}</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
And run the generator:
|
||||
|
||||
```bash
|
||||
$ docker run --rm \
|
||||
-v "$(pwd):/opt:rw" \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
tarampampam/error-pages build --config-file ./error-pages.yml ./out
|
||||
|
||||
$ tree
|
||||
.
|
||||
├── error-pages.yml
|
||||
├── foo.html
|
||||
└── out
|
||||
├── bar
|
||||
│ ├── 400.html
|
||||
│ └── 401.html
|
||||
└── foo
|
||||
├── 400.html
|
||||
└── 401.html
|
||||
|
||||
3 directories, 6 files
|
||||
|
||||
$ cat ./out/foo/400.html
|
||||
<html>
|
||||
<title>400</title>
|
||||
<body>
|
||||
<h1>Bad Request: The server did not understand the request</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
$ cat ./out/bar/400.html
|
||||
Error 400: Bad Request (The server did not understand the request)
|
||||
```
|
||||
|
||||
To see the usage help run the following command:
|
||||
|
||||
```bash
|
||||
$ docker run --rm tarampampam/error-pages build --help
|
||||
```
|
||||
|
||||
### Static error pages
|
||||
|
||||
You may want to use the generated error pages somewhere else, and you can simply extract them from the docker image to your local directory for this purpose:
|
||||
|
||||
```bash
|
||||
$ docker create --name error-pages tarampampam/error-pages:2.0.0-rc2
|
||||
$ docker cp error-pages:/opt/html ./out
|
||||
$ docker rm -f error-pages
|
||||
$ ls ./out
|
||||
ghost hacker-terminal index.html l7-dark l7-light noise shuffle
|
||||
$ tree
|
||||
.
|
||||
└── out
|
||||
├── ghost
|
||||
│ ├── 400.html
|
||||
│ ├── ...
|
||||
│ └── 505.html
|
||||
├── hacker-terminal
|
||||
│ ├── 400.html
|
||||
│ ├── ...
|
||||
│ └── 505.html
|
||||
├── index.html
|
||||
├── l7-dark
|
||||
│ ├── 400.html
|
||||
│ ├── ...
|
||||
...
|
||||
```
|
||||
|
||||
### Custom error pages for your image with [nginx][link_nginx]
|
||||
|
||||
@ -92,12 +220,12 @@ server {
|
||||
```dockerfile
|
||||
# File `Dockerfile`
|
||||
|
||||
FROM nginx:1.18-alpine
|
||||
FROM nginx:1.21-alpine
|
||||
|
||||
COPY --chown=nginx \
|
||||
./nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --chown=nginx \
|
||||
--from=tarampampam/error-pages:1.7.0 \
|
||||
--from=tarampampam/error-pages:2.0.0 \
|
||||
/opt/html/ghost /usr/share/nginx/errorpages/_error-pages
|
||||
```
|
||||
|
||||
@ -107,16 +235,29 @@ $ docker build --tag your-nginx:local -f ./Dockerfile .
|
||||
|
||||
> More info about `error_page` directive can be [found here](http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page).
|
||||
|
||||
### Custom error pages for [Traefik][link_traefik]
|
||||
## Templates
|
||||
|
||||
Simple traefik (tested on `v2.4.8`) service configuration for usage in [docker swarm][link_swarm] (**change with your needs**):
|
||||
Name | Preview
|
||||
:---------------: | :-----:
|
||||
`ghost` | [![ghost](https://hsto.org/webt/oj/cl/4k/ojcl4ko_cvusy5xuki6efffzsyo.gif)](https://tarampampam.github.io/error-pages/ghost/404.html)
|
||||
`l7-light` | [![l7-light](https://hsto.org/webt/xc/iq/vt/xciqvty-aoj-rchfarsjhutpjny.png)](https://tarampampam.github.io/error-pages/l7-light/404.html)
|
||||
`l7-dark` | [![l7-dark](https://hsto.org/webt/s1/ih/yr/s1ihyrqs_y-sgraoimfhk6ypney.png)](https://tarampampam.github.io/error-pages/l7-dark/404.html)
|
||||
`shuffle` | [![shuffle](https://hsto.org/webt/7w/rk/3m/7wrk3mrzz3y8qfqwovmuvacu-bs.gif)](https://tarampampam.github.io/error-pages/shuffle/404.html)
|
||||
`noise` | [![noise](https://hsto.org/webt/42/oq/8y/42oq8yok_i-arrafjt6hds_7ahy.gif)](https://tarampampam.github.io/error-pages/noise/404.html)
|
||||
`hacker-terminal` | [![hacker-terminal](https://hsto.org/webt/5s/l0/p1/5sl0p1_ud_nalzjzsj5slz6dfda.gif)](https://tarampampam.github.io/error-pages/hacker-terminal/404.html)
|
||||
|
||||
> Note: `noise` template highly uses the CPU, be careful
|
||||
|
||||
## Custom error pages for [Traefik][link_traefik]
|
||||
|
||||
Simple traefik (tested on `v2.5.3`) service configuration for usage in [docker swarm][link_swarm] (**change with your needs**):
|
||||
|
||||
```yaml
|
||||
version: '3.4'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
error-pages:
|
||||
image: tarampampam/error-pages:1.7.0
|
||||
image: tarampampam/error-pages:2.0.0
|
||||
environment:
|
||||
TEMPLATE_NAME: l7-dark
|
||||
networks:
|
||||
@ -164,28 +305,6 @@ networks:
|
||||
external: true
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
> For project development we use `docker-ce` + `docker-compose`. Make sure you have them installed.
|
||||
|
||||
Install "generator" dependencies:
|
||||
|
||||
```bash
|
||||
$ make install
|
||||
```
|
||||
|
||||
If you want to generate error pages on your machine _(after that look into the output directory)_:
|
||||
|
||||
```bash
|
||||
$ make gen
|
||||
```
|
||||
|
||||
If you want to preview the pages using the Docker image:
|
||||
|
||||
```bash
|
||||
$ make preview
|
||||
```
|
||||
|
||||
## Changes log
|
||||
|
||||
[![Release date][badge_release_date]][link_releases]
|
||||
@ -204,24 +323,35 @@ If you will find any package errors, please, [make an issue][link_create_issue]
|
||||
|
||||
This is open-sourced software licensed under the [MIT License][link_license].
|
||||
|
||||
[badge_build_status]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests/master
|
||||
[badge_release_date]:https://img.shields.io/github/release-date/tarampampam/error-pages.svg?style=flat-square&maxAge=180
|
||||
[badge_commits_since_release]:https://img.shields.io/github/commits-since/tarampampam/error-pages/latest.svg?style=flat-square&maxAge=180
|
||||
[badge_issues]:https://img.shields.io/github/issues/tarampampam/error-pages.svg?style=flat-square&maxAge=180
|
||||
[badge_pulls]:https://img.shields.io/github/issues-pr/tarampampam/error-pages.svg?style=flat-square&maxAge=180
|
||||
[badge_license]:https://img.shields.io/github/license/tarampampam/error-pages.svg?longCache=true
|
||||
[badge_build]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/tests?maxAge=30&label=tests&logo=github
|
||||
[badge_release]:https://img.shields.io/github/workflow/status/tarampampam/error-pages/release?maxAge=30&label=release&logo=github
|
||||
[badge_coverage]:https://img.shields.io/codecov/c/github/tarampampam/error-pages/master.svg?maxAge=30
|
||||
[badge_release_version]:https://img.shields.io/github/release/tarampampam/error-pages.svg?maxAge=30
|
||||
[badge_size_latest]:https://img.shields.io/docker/image-size/tarampampam/error-pages/latest?maxAge=30
|
||||
[badge_docker_pulls]:https://img.shields.io/docker/pulls/tarampampam/error-pages.svg
|
||||
[badge_language]:https://img.shields.io/github/go-mod/go-version/tarampampam/error-pages?longCache=true
|
||||
[badge_license]:https://img.shields.io/github/license/tarampampam/error-pages.svg?longCache=true
|
||||
[badge_release_date]:https://img.shields.io/github/release-date/tarampampam/error-pages.svg?maxAge=180
|
||||
[badge_commits_since_release]:https://img.shields.io/github/commits-since/tarampampam/error-pages/latest.svg?maxAge=45
|
||||
[badge_issues]:https://img.shields.io/github/issues/tarampampam/error-pages.svg?maxAge=45
|
||||
[badge_pulls]:https://img.shields.io/github/issues-pr/tarampampam/error-pages.svg?maxAge=45
|
||||
|
||||
[link_coverage]:https://codecov.io/gh/tarampampam/error-pages
|
||||
[link_build]:https://github.com/tarampampam/error-pages/actions
|
||||
[link_docker_hub]:https://hub.docker.com/r/tarampampam/error-pages
|
||||
[link_docker_tags]:https://hub.docker.com/r/tarampampam/error-pages/tags
|
||||
[link_license]:https://github.com/tarampampam/error-pages/blob/master/LICENSE
|
||||
[link_releases]:https://github.com/tarampampam/error-pages/releases
|
||||
[link_commits]:https://github.com/tarampampam/error-pages/commits
|
||||
[link_changes_log]:https://github.com/tarampampam/error-pages/blob/master/CHANGELOG.md
|
||||
[link_issues]:https://github.com/tarampampam/error-pages/issues
|
||||
[link_create_issue]:https://github.com/tarampampam/error-pages/issues/new/choose
|
||||
[link_pulls]:https://github.com/tarampampam/error-pages/pulls
|
||||
[link_build_status]:https://travis-ci.org/tarampampam/error-pages
|
||||
[link_create_issue]:https://github.com/tarampampam/error-pages/issues/new
|
||||
[link_license]:https://github.com/tarampampam/error-pages/blob/master/LICENSE
|
||||
[link_docker_hub]:https://hub.docker.com/r/tarampampam/error-pages/
|
||||
[link_ghcr]:https://github.com/users/tarampampam/packages/container/package/error-pages
|
||||
|
||||
[fasthttp]:https://github.com/valyala/fasthttp
|
||||
[preview-sources]:https://github.com/tarampampam/error-pages/tree/gh-pages
|
||||
[preview-demo]:https://tarampampam.github.io/error-pages/
|
||||
|
||||
[link_nginx]:http://nginx.org/
|
||||
[link_traefik]:https://docs.traefik.io/
|
||||
[link_swarm]:https://docs.docker.com/engine/swarm/
|
||||
|
29
cmd/error-pages/main.go
Normal file
29
cmd/error-pages/main.go
Normal file
@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/tarampampam/error-pages/internal/cli"
|
||||
)
|
||||
|
||||
// exitFn is a function for application exiting.
|
||||
var exitFn = os.Exit //nolint:gochecknoglobals
|
||||
|
||||
// main CLI application entrypoint.
|
||||
func main() { exitFn(run()) }
|
||||
|
||||
// run this CLI application.
|
||||
// Exit codes documentation: <https://tldp.org/LDP/abs/html/exitcodes.html>
|
||||
func run() int {
|
||||
cmd := cli.NewCommand(filepath.Base(os.Args[0]))
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
_, _ = color.New(color.FgHiRed, color.Bold).Fprintln(os.Stderr, err.Error())
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
133
config.json
133
config.json
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,16 +1,46 @@
|
||||
version: '3.2'
|
||||
# Docker-compose file is used only for local development. This is not production-ready example.
|
||||
|
||||
version: '3.4'
|
||||
|
||||
volumes:
|
||||
tmp-data:
|
||||
tmp-data: {}
|
||||
golint-cache: {}
|
||||
|
||||
services:
|
||||
node:
|
||||
image: node:15.14-alpine # Image page: <https://hub.docker.com/_/node>
|
||||
app: &app-service
|
||||
image: golang:1.17.1-buster # Image page: <https://hub.docker.com/_/golang>
|
||||
working_dir: /src
|
||||
environment:
|
||||
HOME: /tmp
|
||||
PS1: '\[\033[1;32m\]\[\033[1;36m\][\u@docker] \[\033[1;34m\]\w\[\033[0;35m\] \[\033[1;36m\]# \[\033[0m\]'
|
||||
GOPATH: /tmp
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
- .:/src:cached
|
||||
- .:/src:rw
|
||||
- tmp-data:/tmp:rw
|
||||
|
||||
web:
|
||||
<<: *app-service
|
||||
ports:
|
||||
- "8080:8080/tcp" # Open <http://127.0.0.1:8080>
|
||||
command:
|
||||
- go
|
||||
- run
|
||||
- ./cmd/error-pages
|
||||
- serve
|
||||
- --verbose
|
||||
- --port=8080
|
||||
healthcheck:
|
||||
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/health/live']
|
||||
interval: 5s
|
||||
timeout: 2s
|
||||
|
||||
golint:
|
||||
image: golangci/golangci-lint:v1.42-alpine # Image page: <https://hub.docker.com/r/golangci/golangci-lint>
|
||||
environment:
|
||||
GOLANGCI_LINT_CACHE: /tmp/golint # <https://github.com/golangci/golangci-lint/blob/v1.42.0/internal/cache/default.go#L68>
|
||||
volumes:
|
||||
- .:/src:ro
|
||||
- golint-cache:/tmp/golint:rw
|
||||
working_dir: /src
|
||||
command: /bin/true
|
||||
|
@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
# on `docker restart` next directory keep existing: <https://github.com/tarampampam/error-pages/issues/3>
|
||||
if [ -d /opt/html/nginx-error-pages ]; then
|
||||
rm -Rf /opt/html/nginx-error-pages;
|
||||
fi;
|
||||
|
||||
# allows to use random template
|
||||
if [ ! -z "$TEMPLATE_NAME" ] && ([ "$TEMPLATE_NAME" = "random" ] || [ "$TEMPLATE_NAME" = "RANDOM" ]); then
|
||||
# find all templates in directory (only template directories must be located in /opt/html)
|
||||
allowed_templates=$(find /opt/html/* -maxdepth 1 -type d -exec basename {} \;);
|
||||
|
||||
# pick random template name
|
||||
random_template_name=$(shuf -e -n1 $allowed_templates);
|
||||
|
||||
echo "$0: Use '$random_template_name' as randomly selected template";
|
||||
|
||||
TEMPLATE_NAME="$random_template_name"
|
||||
fi;
|
||||
|
||||
TEMPLATE_NAME=${TEMPLATE_NAME:-ghost} # string|empty
|
||||
|
||||
echo "$0: Set pages for template '$TEMPLATE_NAME' as default (make accessible in root directory)";
|
||||
|
||||
# check for template existing
|
||||
if [ ! -d "/opt/html/$TEMPLATE_NAME" ]; then
|
||||
echo >&3 "$0: Template '$TEMPLATE_NAME' was not found!";
|
||||
exit 1;
|
||||
fi;
|
||||
|
||||
# allows "direct access" to the error pages using URLs like "/500.html"
|
||||
ln -f -s "/opt/html/$TEMPLATE_NAME/"* /opt/html;
|
||||
|
||||
# next directory is required for easy nginx `error_page` usage
|
||||
mkdir /opt/html/nginx-error-pages;
|
||||
|
||||
# use error pages from the template as "native" nginx error pages
|
||||
ln -f -s "/opt/html/$TEMPLATE_NAME/"* /opt/html/nginx-error-pages;
|
||||
|
||||
exit 0
|
@ -1,32 +0,0 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
index index.html index.htm;
|
||||
root /opt/html;
|
||||
|
||||
error_page 400 /nginx-error-pages/400.html;
|
||||
error_page 401 /nginx-error-pages/401.html;
|
||||
error_page 403 /nginx-error-pages/403.html;
|
||||
error_page 404 /nginx-error-pages/404.html;
|
||||
error_page 500 /nginx-error-pages/500.html;
|
||||
error_page 502 /nginx-error-pages/502.html;
|
||||
|
||||
location ^~ /nginx-error-pages/ {
|
||||
internal;
|
||||
root /opt/html;
|
||||
}
|
||||
|
||||
# Health-check (liveness probe)
|
||||
location = /health/live {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "healthy\n";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri =404;
|
||||
}
|
||||
}
|
93
error-pages.yml
Normal file
93
error-pages.yml
Normal file
@ -0,0 +1,93 @@
|
||||
templates:
|
||||
# - name: {string} Template name (optional, if path is defined)
|
||||
# path: {string} Path to the template file
|
||||
# content: {string} Template content, if path is not defined
|
||||
- path: ./templates/ghost.html
|
||||
name: ghost # name is optional, if path is defined
|
||||
content: ${GHOST_TEMPLATE_CONTENT}
|
||||
- path: ./templates/l7-light.html
|
||||
- path: ./templates/l7-dark.html
|
||||
- path: ./templates/shuffle.html
|
||||
- path: ./templates/noise.html
|
||||
- path: ./templates/hacker-terminal.html
|
||||
|
||||
pages:
|
||||
400:
|
||||
message: Bad Request
|
||||
description: The server did not understand the request
|
||||
|
||||
401:
|
||||
message: Unauthorized
|
||||
description: The requested page needs a username and a password
|
||||
|
||||
403:
|
||||
message: Forbidden
|
||||
description: Access is forbidden to the requested page
|
||||
|
||||
404:
|
||||
message: Not Found
|
||||
description: The server can not find the requested page
|
||||
|
||||
405:
|
||||
message: Method Not Allowed
|
||||
description: The method specified in the request is not allowed
|
||||
|
||||
407:
|
||||
message: Proxy Authentication Required
|
||||
description: You must authenticate with a proxy server before this request can be served
|
||||
|
||||
408:
|
||||
message: Request Timeout
|
||||
description: The request took longer than the server was prepared to wait
|
||||
|
||||
409:
|
||||
message: Conflict
|
||||
description: The request could not be completed because of a conflict
|
||||
|
||||
410:
|
||||
message: Gone
|
||||
description: The requested page is no longer available
|
||||
|
||||
411:
|
||||
message: Length Required
|
||||
description: The "Content-Length" is not defined. The server will not accept the request without it
|
||||
|
||||
412:
|
||||
message: Precondition Failed
|
||||
description: The pre condition given in the request evaluated to false by the server
|
||||
|
||||
413:
|
||||
message: Payload Too Large
|
||||
description: The server will not accept the request, because the request entity is too large
|
||||
|
||||
416:
|
||||
message: Requested Range Not Satisfiable
|
||||
description: The requested byte range is not available and is out of bounds
|
||||
|
||||
418:
|
||||
message: I'm a teapot
|
||||
description: Attempt to brew coffee with a teapot is not supported
|
||||
|
||||
429:
|
||||
message: Too Many Requests
|
||||
description: Too many requests in a given amount of time
|
||||
|
||||
500:
|
||||
message: Internal Server Error
|
||||
description: The server met an unexpected condition
|
||||
|
||||
502:
|
||||
message: Bad Gateway
|
||||
description: The server received an invalid response from the upstream server
|
||||
|
||||
503:
|
||||
message: Service Unavailable
|
||||
description: The server is temporarily overloading or down
|
||||
|
||||
504:
|
||||
message: Gateway Timeout
|
||||
description: The gateway has timed out
|
||||
|
||||
505:
|
||||
message: HTTP Version Not Supported
|
||||
description: The server does not support the "http protocol" version
|
9
generator/.gitignore
vendored
9
generator/.gitignore
vendored
@ -1,9 +0,0 @@
|
||||
## Vendors
|
||||
/node_modules
|
||||
|
||||
## Lock files (use yarn only)
|
||||
package-lock.json
|
||||
|
||||
## Temp dirs & trash
|
||||
npm-debug.log
|
||||
yarn-error.log
|
@ -1,130 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yargs = require('yargs');
|
||||
|
||||
const options = yargs
|
||||
.usage('Usage: -c <config.json> -d <output-directory>')
|
||||
.option("c", {alias: "config", describe: "config file path", type: "string", demandOption: true})
|
||||
.option("o", {alias: "out", describe: "output directory path", type: "string", demandOption: true})
|
||||
.option("i", {alias: "index", describe: "generate index page", type: "boolean"})
|
||||
.argv;
|
||||
|
||||
const configFile = options.config;
|
||||
const outDir = options.out;
|
||||
const generateIndexPage = options.index;
|
||||
|
||||
const generated = {};
|
||||
|
||||
try {
|
||||
// Make sure that config file exists
|
||||
if (!fs.existsSync(configFile)) {
|
||||
throw new Error(`Config file "${configFile}" was not found`);
|
||||
}
|
||||
|
||||
// Create output directory (if needed)
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir);
|
||||
}
|
||||
|
||||
// Read JSON config file and parse into object
|
||||
const configContent = JSON.parse(fs.readFileSync(configFile));
|
||||
|
||||
// Loop over all defined templates in configuration file
|
||||
configContent.templates.forEach((templateConfig) => {
|
||||
// Make sure that template layout file exists
|
||||
if (!fs.existsSync(templateConfig.path)) {
|
||||
throw new Error(`Template "${templateConfig.name}" was not found in "${templateConfig.path}"`);
|
||||
}
|
||||
|
||||
// Read layout content into memory prepare output directory for template
|
||||
const layoutContent = String(fs.readFileSync(templateConfig.path));
|
||||
const templateOutDir = path.join(outDir, templateConfig.name);
|
||||
|
||||
if (!fs.existsSync(templateOutDir)) {
|
||||
fs.mkdirSync(templateOutDir);
|
||||
}
|
||||
|
||||
console.info(`Use template "${templateConfig.name}" located in "${templateConfig.path}"`);
|
||||
|
||||
// Loop over all pages
|
||||
configContent.pages.forEach((pageConfig) => {
|
||||
let outFileName = pageConfig.code + "." + configContent.output.file_extension,
|
||||
outPath = path.join(templateOutDir, outFileName);
|
||||
|
||||
console.info(` [${templateConfig.name}:${pageConfig.code}] Output: ${outPath}`);
|
||||
|
||||
// Make replaces
|
||||
let result = layoutContent
|
||||
.replace(/{{\s?code\s?}}/g, pageConfig.code)
|
||||
.replace(/{{\s?message\s?}}/g, pageConfig.message)
|
||||
.replace(/{{\s?description\s?}}/g, pageConfig.description);
|
||||
|
||||
// And write into result file
|
||||
fs.writeFileSync(outPath, result, {
|
||||
encoding: "utf8",
|
||||
flag: "w+",
|
||||
mode: 0o644
|
||||
});
|
||||
|
||||
if (!generated[templateConfig.name]) {
|
||||
generated[templateConfig.name] = [];
|
||||
}
|
||||
|
||||
generated[templateConfig.name].push({
|
||||
code: pageConfig.code,
|
||||
message: pageConfig.message,
|
||||
description: pageConfig.description,
|
||||
path: path.join(templateConfig.name, outFileName),
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
// Generate index page for the generated content
|
||||
if (generateIndexPage === true) {
|
||||
const indexPageFilePath = path.join(outDir, 'index.html');
|
||||
|
||||
let content = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<title>Error pages list</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.6.0/css/bootstrap.min.css"
|
||||
integrity="sha512-P5MgMn1jBN01asBgU0z60Qk4QxiXo86+wlFahKrsQf37c9cro517WzVSPPV1tDKzhku2iJ2FVgL67wG03SGnNA=="
|
||||
crossorigin="anonymous" />
|
||||
</head>
|
||||
<body>
|
||||
<main role="main" class="container">\n`;
|
||||
|
||||
Object.keys(generated).forEach(function(templateName) {
|
||||
content += `<h2 class="mt-5">Template name: <code>` + templateName + `</code></h2>\n<ul class="mb-5">\n`;
|
||||
|
||||
generated[templateName].forEach((properties) => {
|
||||
content += ` <li><a href="${properties.path}"><span class="badge badge-light">${properties.code}</span>: ${properties.message}</a></li>\n`;
|
||||
})
|
||||
|
||||
content += `</ul>\n`;
|
||||
});
|
||||
|
||||
content += `</main>
|
||||
<footer class="footer">
|
||||
<div class="container text-center text-muted mt-3 mb-3">
|
||||
For online documentation and support please refer to the <a href="https://github.com/tarampampam/error-pages">project repository</a>.
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
fs.writeFileSync(indexPageFilePath, content, {
|
||||
encoding: "utf8",
|
||||
flag: "w+",
|
||||
mode: 0o644
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
process.exit(1);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "error-pages",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/tarampampam/error-pages.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"yargs": "16.2"
|
||||
}
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
ansi-regex@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
|
||||
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
|
||||
|
||||
ansi-styles@^4.0.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
cliui@^7.0.2:
|
||||
version "7.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
|
||||
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
is-fullwidth-code-point@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||
|
||||
require-directory@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
|
||||
integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
strip-ansi@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
|
||||
integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||
|
||||
yargs-parser@^20.2.2:
|
||||
version "20.2.7"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
|
||||
integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
|
||||
|
||||
yargs@16.2:
|
||||
version "16.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
|
||||
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
|
||||
dependencies:
|
||||
cliui "^7.0.2"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.0"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
32
go.mod
Normal file
32
go.mod
Normal file
@ -0,0 +1,32 @@
|
||||
module github.com/tarampampam/error-pages
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/a8m/envsubst v1.2.0
|
||||
github.com/fasthttp/router v1.4.3
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/valyala/fasthttp v1.30.0
|
||||
go.uber.org/zap v1.19.1
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
|
||||
)
|
615
go.sum
Normal file
615
go.sum
Normal file
@ -0,0 +1,615 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/a8m/envsubst v1.2.0 h1:yvzAhJD2QKdo35Ut03wIfXQmg+ta3wC/1bskfZynz+Q=
|
||||
github.com/a8m/envsubst v1.2.0/go.mod h1:PpvLvNWa+Rvu/10qXmFbFiGICIU5hZvFJNPCCkUaObg=
|
||||
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.3 h1:fpcw+r1N1h0Poc1F/pHbW40cUm/lMEQslZtCkBQ0UnM=
|
||||
github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fasthttp/router v1.4.3 h1:spS+LUnRryQ/+hbmYzs/xWGJlQCkeQI3hxGZdlVYhLU=
|
||||
github.com/fasthttp/router v1.4.3/go.mod h1:9ytWCfZ5LcCcbD3S7pEXyBX9vZnOZmN918WiiaYUzr8=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0=
|
||||
github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/savsgio/gotils v0.0.0-20210907153846-c06938798b52/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
|
||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8=
|
||||
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
|
||||
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.30.0 h1:nBNzWrgZUUHohyLPU/jTvXdhrcaf2m5k3bWk+3Q049g=
|
||||
github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
54
internal/breaker/os_signal.go
Normal file
54
internal/breaker/os_signal.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Package breaker provides OSSignals struct for OS signals handling (with context).
|
||||
package breaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// OSSignals allows to subscribe for system signals.
|
||||
type OSSignals struct {
|
||||
ctx context.Context
|
||||
ch chan os.Signal
|
||||
}
|
||||
|
||||
// NewOSSignals creates new subscriber for system signals.
|
||||
func NewOSSignals(ctx context.Context) OSSignals {
|
||||
return OSSignals{
|
||||
ctx: ctx,
|
||||
ch: make(chan os.Signal, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe for some of system signals (call Stop for stopping).
|
||||
func (oss *OSSignals) Subscribe(onSignal func(os.Signal), signals ...os.Signal) {
|
||||
if len(signals) == 0 {
|
||||
signals = []os.Signal{os.Interrupt, syscall.SIGINT, syscall.SIGTERM} // default signals
|
||||
}
|
||||
|
||||
signal.Notify(oss.ch, signals...)
|
||||
|
||||
go func(ch <-chan os.Signal) {
|
||||
select {
|
||||
case <-oss.ctx.Done():
|
||||
break
|
||||
|
||||
case sig, opened := <-ch:
|
||||
if oss.ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if opened && sig != nil {
|
||||
onSignal(sig)
|
||||
}
|
||||
}
|
||||
}(oss.ch)
|
||||
}
|
||||
|
||||
// Stop system signals listening.
|
||||
func (oss *OSSignals) Stop() {
|
||||
signal.Stop(oss.ch)
|
||||
close(oss.ch)
|
||||
}
|
56
internal/breaker/os_signal_test.go
Normal file
56
internal/breaker/os_signal_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package breaker_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/breaker"
|
||||
)
|
||||
|
||||
func TestNewOSSignals(t *testing.T) {
|
||||
oss := breaker.NewOSSignals(context.Background())
|
||||
|
||||
gotSignal := make(chan os.Signal, 1)
|
||||
|
||||
oss.Subscribe(func(signal os.Signal) {
|
||||
gotSignal <- signal
|
||||
}, syscall.SIGUSR2)
|
||||
|
||||
defer oss.Stop()
|
||||
|
||||
proc, err := os.FindProcess(os.Getpid())
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, proc.Signal(syscall.SIGUSR2)) // send the signal
|
||||
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
|
||||
assert.Equal(t, syscall.SIGUSR2, <-gotSignal)
|
||||
}
|
||||
|
||||
func TestNewOSSignalCtxCancel(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
oss := breaker.NewOSSignals(ctx)
|
||||
|
||||
gotSignal := make(chan os.Signal, 1)
|
||||
|
||||
oss.Subscribe(func(signal os.Signal) {
|
||||
gotSignal <- signal
|
||||
}, syscall.SIGUSR2)
|
||||
|
||||
defer oss.Stop()
|
||||
|
||||
proc, err := os.FindProcess(os.Getpid())
|
||||
assert.NoError(t, err)
|
||||
|
||||
cancel()
|
||||
|
||||
assert.NoError(t, proc.Signal(syscall.SIGUSR2)) // send the signal
|
||||
|
||||
assert.Empty(t, gotSignal)
|
||||
}
|
56
internal/checkers/health.go
Normal file
56
internal/checkers/health.go
Normal file
@ -0,0 +1,56 @@
|
||||
package checkers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type httpClient interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// HealthChecker is a heals checker.
|
||||
type HealthChecker struct {
|
||||
ctx context.Context
|
||||
httpClient httpClient
|
||||
}
|
||||
|
||||
const defaultHTTPClientTimeout = time.Second * 3
|
||||
|
||||
// NewHealthChecker creates heals checker.
|
||||
func NewHealthChecker(ctx context.Context, client ...httpClient) *HealthChecker {
|
||||
var c httpClient
|
||||
|
||||
if len(client) == 1 {
|
||||
c = client[0]
|
||||
} else {
|
||||
c = &http.Client{Timeout: defaultHTTPClientTimeout} // default
|
||||
}
|
||||
|
||||
return &HealthChecker{ctx: ctx, httpClient: c}
|
||||
}
|
||||
|
||||
// Check application using liveness probe.
|
||||
func (c *HealthChecker) Check(port uint16) error {
|
||||
req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/health/live", port), nil) //nolint:lll
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "HealthChecker/internal")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if code := resp.StatusCode; code != http.StatusOK {
|
||||
return fmt.Errorf("wrong status code [%d] from live endpoint", code)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
48
internal/checkers/health_test.go
Normal file
48
internal/checkers/health_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package checkers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
)
|
||||
|
||||
type httpClientFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f httpClientFunc) Do(req *http.Request) (*http.Response, error) { return f(req) }
|
||||
|
||||
func TestHealthChecker_CheckSuccess(t *testing.T) {
|
||||
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, http.MethodGet, req.Method)
|
||||
assert.Equal(t, "http://127.0.0.1:123/health/live", req.URL.String())
|
||||
assert.Equal(t, "HealthChecker/internal", req.Header.Get("User-Agent"))
|
||||
|
||||
return &http.Response{
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
StatusCode: http.StatusOK,
|
||||
}, nil
|
||||
}
|
||||
|
||||
checker := checkers.NewHealthChecker(context.Background(), httpMock)
|
||||
|
||||
assert.NoError(t, checker.Check(123))
|
||||
}
|
||||
|
||||
func TestHealthChecker_CheckFail(t *testing.T) {
|
||||
var httpMock httpClientFunc = func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
Body: ioutil.NopCloser(bytes.NewReader([]byte{})),
|
||||
StatusCode: http.StatusBadGateway,
|
||||
}, nil
|
||||
}
|
||||
|
||||
checker := checkers.NewHealthChecker(context.Background(), httpMock)
|
||||
|
||||
err := checker.Check(123)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "wrong status code")
|
||||
}
|
10
internal/checkers/live.go
Normal file
10
internal/checkers/live.go
Normal file
@ -0,0 +1,10 @@
|
||||
package checkers
|
||||
|
||||
// LiveChecker is a liveness checker.
|
||||
type LiveChecker struct{}
|
||||
|
||||
// NewLiveChecker creates liveness checker.
|
||||
func NewLiveChecker() *LiveChecker { return &LiveChecker{} }
|
||||
|
||||
// Check application is alive?
|
||||
func (*LiveChecker) Check() error { return nil }
|
12
internal/checkers/live_test.go
Normal file
12
internal/checkers/live_test.go
Normal file
@ -0,0 +1,12 @@
|
||||
package checkers_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
)
|
||||
|
||||
func TestLiveChecker_Check(t *testing.T) {
|
||||
assert.NoError(t, checkers.NewLiveChecker().Check())
|
||||
}
|
198
internal/cli/build/command.go
Normal file
198
internal/cli/build/command.go
Normal file
@ -0,0 +1,198 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type historyItem struct {
|
||||
Code, Message, Path string
|
||||
}
|
||||
|
||||
// NewCommand creates `build` command.
|
||||
func NewCommand(log *zap.Logger, configFile *string) *cobra.Command { //nolint:funlen,gocognit
|
||||
var (
|
||||
generateIndex bool
|
||||
cfg *config.Config
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "build <output-directory>",
|
||||
Aliases: []string{"b"},
|
||||
Short: "Build the error pages",
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: func(*cobra.Command, []string) error {
|
||||
if configFile == nil {
|
||||
return errors.New("path to the config file is required for this command")
|
||||
}
|
||||
|
||||
if c, err := config.FromYamlFile(*configFile); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err = c.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg = c
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("wrong arguments count")
|
||||
}
|
||||
|
||||
log.Info("loading templates")
|
||||
|
||||
templates, err := cfg.LoadTemplates()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(templates) == 0 {
|
||||
return errors.New("no loaded templates")
|
||||
}
|
||||
|
||||
log.Debug("the output directory preparing", zap.String("Path", args[0]))
|
||||
|
||||
if err = createDirectory(args[0]); err != nil {
|
||||
return errors.Wrap(err, "cannot prepare output directory")
|
||||
}
|
||||
|
||||
codes := make(map[string]tpl.Annotator)
|
||||
|
||||
for code, desc := range cfg.Pages {
|
||||
codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
|
||||
}
|
||||
|
||||
history := make(map[string][]historyItem, len(templates))
|
||||
|
||||
log.Info("saving the error pages")
|
||||
startedAt := time.Now()
|
||||
|
||||
if err = tpl.NewErrors(templates, codes).VisitAll(func(template, code string, content []byte) error {
|
||||
if e := createDirectory(path.Join(args[0], template)); e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
fileName := code + ".html"
|
||||
|
||||
if e := os.WriteFile(path.Join(args[0], template, fileName), content, 0664); e != nil { //nolint:gosec,gomnd
|
||||
return e
|
||||
}
|
||||
|
||||
if _, ok := history[template]; !ok {
|
||||
history[template] = make([]historyItem, 0, len(codes))
|
||||
}
|
||||
|
||||
history[template] = append(history[template], historyItem{
|
||||
Code: code,
|
||||
Message: codes[code].Message,
|
||||
Path: path.Join(template, fileName),
|
||||
})
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("saved", zap.Duration("duration", time.Since(startedAt)))
|
||||
|
||||
if generateIndex {
|
||||
log.Info("index file generation")
|
||||
startedAt = time.Now()
|
||||
|
||||
if err = writeIndexFile(path.Join(args[0], "index.html"), history); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("index file generated", zap.Duration("duration", time.Since(startedAt)))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(
|
||||
&generateIndex,
|
||||
"index", "i",
|
||||
false,
|
||||
"generate index page",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func createDirectory(path string) error {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(path, 0775) //nolint:gomnd
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if !stat.IsDir() {
|
||||
return errors.New("is not a directory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeIndexFile(path string, history map[string][]historyItem) error {
|
||||
t, err := template.New("index").Parse(`<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<title>Error pages list</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.1.1/css/bootstrap.min.css"
|
||||
integrity="sha512-6KY5s6UI5J7SVYuZB4S/CZMyPylqyyNZco376NM2Z8Sb8OxEdp02e1jkKk/wZxIEmjQ6DRCEBhni+gpr9c4tvA=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<main>
|
||||
<div class="py-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="https://hsto.org/webt/rm/9y/ww/rm9ywwx3gjv9agwkcmllhsuyo7k.png"
|
||||
alt="" width="94">
|
||||
<h2>Error pages index</h2>
|
||||
</div>
|
||||
{{- range $template, $item := . -}}
|
||||
<h2 class="mb-3">Template name: <Code>{{ $template }}</Code></h2>
|
||||
<ul class="mb-5">
|
||||
{{ range $item -}}
|
||||
<li><a href="{{ .Path }}"><strong>{{ .Code }}</strong>: {{ .Message }}</a></li>
|
||||
{{ end -}}
|
||||
</ul>
|
||||
{{ end }}
|
||||
</main>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container text-center text-muted mt-3 mb-3">
|
||||
For online documentation and support please refer to the
|
||||
<a href="https://github.com/tarampampam/error-pages">project repository</a>.
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err = t.Execute(&buf, history); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, buf.Bytes(), 0664) //nolint:gosec,gomnd
|
||||
}
|
7
internal/cli/build/command_test.go
Normal file
7
internal/cli/build/command_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package build_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
57
internal/cli/healthcheck/command.go
Normal file
57
internal/cli/healthcheck/command.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Package healthcheck contains CLI `healthcheck` command implementation.
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
)
|
||||
|
||||
type checker interface {
|
||||
Check(port uint16) error
|
||||
}
|
||||
|
||||
const portFlagName = "port"
|
||||
|
||||
// NewCommand creates `healthcheck` command.
|
||||
func NewCommand(checker checker) *cobra.Command {
|
||||
var port uint16
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "healthcheck",
|
||||
Aliases: []string{"chk", "health", "check"},
|
||||
Short: "Health checker for the HTTP server. Use case - docker healthcheck",
|
||||
PreRunE: func(c *cobra.Command, _ []string) (lastErr error) {
|
||||
c.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
// flag was NOT defined using CLI (flags should have maximal priority)
|
||||
if !flag.Changed && flag.Name == portFlagName {
|
||||
if envPort, exists := env.ListenPort.Lookup(); exists && envPort != "" {
|
||||
if p, err := strconv.ParseUint(envPort, 10, 16); err == nil { //nolint:gomnd
|
||||
port = uint16(p)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return lastErr
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
return checker.Check(port)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Uint16VarP(
|
||||
&port,
|
||||
portFlagName,
|
||||
"p",
|
||||
8080, //nolint:gomnd // must be same as default serve `--port` flag value
|
||||
fmt.Sprintf("TCP port number [$%s]", env.ListenPort),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
94
internal/cli/healthcheck/command_test.go
Normal file
94
internal/cli/healthcheck/command_test.go
Normal file
@ -0,0 +1,94 @@
|
||||
package healthcheck_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/cli/healthcheck"
|
||||
)
|
||||
|
||||
type fakeChecker struct{ err error }
|
||||
|
||||
func (c *fakeChecker) Check(port uint16) error { return c.err }
|
||||
|
||||
func TestProperties(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
|
||||
assert.Equal(t, "healthcheck", cmd.Use)
|
||||
assert.ElementsMatch(t, []string{"chk", "health", "check"}, cmd.Aliases)
|
||||
assert.NotNil(t, cmd.RunE)
|
||||
}
|
||||
|
||||
func TestCommandRun(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureOutput(func() {
|
||||
assert.NoError(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Empty(t, output)
|
||||
}
|
||||
|
||||
func TestCommandRunFailed(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: errors.New("foo err")})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "foo err")
|
||||
}
|
||||
|
||||
func TestPortFlagWrongArgument(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{"-p", "65536"}) // 65535 is max
|
||||
|
||||
var executed bool
|
||||
|
||||
cmd.RunE = func(*cobra.Command, []string) error {
|
||||
executed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "invalid argument")
|
||||
assert.Contains(t, output, "65536")
|
||||
assert.Contains(t, output, "value out of range")
|
||||
assert.False(t, executed)
|
||||
}
|
||||
|
||||
func TestPortFlagWrongEnvValue(t *testing.T) {
|
||||
cmd := healthcheck.NewCommand(&fakeChecker{err: nil})
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
assert.NoError(t, os.Setenv("LISTEN_PORT", "65536")) // 65535 is max
|
||||
|
||||
defer func() { assert.NoError(t, os.Unsetenv("LISTEN_PORT")) }()
|
||||
|
||||
var executed bool
|
||||
|
||||
cmd.RunE = func(*cobra.Command, []string) error {
|
||||
executed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
output := capturer.CaptureStderr(func() {
|
||||
assert.Error(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "wrong TCP port")
|
||||
assert.Contains(t, output, "environment variable")
|
||||
assert.Contains(t, output, "65536")
|
||||
assert.False(t, executed)
|
||||
}
|
93
internal/cli/root.go
Normal file
93
internal/cli/root.go
Normal file
@ -0,0 +1,93 @@
|
||||
// Package cli contains CLI command handlers.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
buildCmd "github.com/tarampampam/error-pages/internal/cli/build"
|
||||
healthcheckCmd "github.com/tarampampam/error-pages/internal/cli/healthcheck"
|
||||
serveCmd "github.com/tarampampam/error-pages/internal/cli/serve"
|
||||
versionCmd "github.com/tarampampam/error-pages/internal/cli/version"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
"github.com/tarampampam/error-pages/internal/version"
|
||||
)
|
||||
|
||||
const configFileFlagName = "config-file"
|
||||
|
||||
// NewCommand creates root command.
|
||||
func NewCommand(appName string) *cobra.Command { //nolint:funlen
|
||||
var (
|
||||
configFile string
|
||||
verbose bool
|
||||
debug bool
|
||||
logJSON bool
|
||||
)
|
||||
|
||||
ctx := context.Background() // main CLI context
|
||||
|
||||
// create "default" logger (will be overwritten later with customized)
|
||||
log, err := logger.New(false, false, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: appName,
|
||||
PersistentPreRunE: func(c *cobra.Command, _ []string) error {
|
||||
_ = log.Sync() // sync previous logger instance
|
||||
|
||||
customizedLog, e := logger.New(verbose, debug, logJSON)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
*log = *customizedLog // override "default" logger with customized
|
||||
|
||||
c.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
// flag was NOT defined using CLI (flags should have maximal priority)
|
||||
if !flag.Changed && flag.Name == configFileFlagName {
|
||||
if envConfigFile, exists := env.ConfigFilePath.Lookup(); exists && envConfigFile != "" {
|
||||
configFile = envConfigFile
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
PersistentPostRun: func(*cobra.Command, []string) {
|
||||
// error ignoring reasons:
|
||||
// - <https://github.com/uber-go/zap/issues/772>
|
||||
// - <https://github.com/uber-go/zap/issues/328>
|
||||
_ = log.Sync()
|
||||
},
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
CompletionOptions: cobra.CompletionOptions{
|
||||
DisableDefaultCmd: true,
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
cmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "debug output")
|
||||
cmd.PersistentFlags().BoolVarP(&logJSON, "log-json", "", false, "logs in JSON format")
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&configFile,
|
||||
configFileFlagName, "c",
|
||||
"./error-pages.yml",
|
||||
fmt.Sprintf("path to the config file [$%s]", env.ConfigFilePath),
|
||||
)
|
||||
|
||||
cmd.AddCommand(
|
||||
versionCmd.NewCommand(version.Version()),
|
||||
healthcheckCmd.NewCommand(checkers.NewHealthChecker(ctx)),
|
||||
buildCmd.NewCommand(log, &configFile),
|
||||
serveCmd.NewCommand(ctx, log, &configFile),
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
84
internal/cli/root_test.go
Normal file
84
internal/cli/root_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/cli"
|
||||
)
|
||||
|
||||
func TestSubcommands(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
|
||||
cases := []struct {
|
||||
giveName string
|
||||
}{
|
||||
{giveName: "build"},
|
||||
{giveName: "version"},
|
||||
{giveName: "healthcheck"},
|
||||
{giveName: "serve"},
|
||||
}
|
||||
|
||||
// get all existing subcommands and put into the map
|
||||
subcommands := make(map[string]*cobra.Command)
|
||||
for _, sub := range cmd.Commands() {
|
||||
subcommands[sub.Name()] = sub
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.giveName, func(t *testing.T) {
|
||||
if _, exists := subcommands[tt.giveName]; !exists {
|
||||
assert.Failf(t, "command not found", "command [%s] was not found", tt.giveName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlags(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
|
||||
cases := []struct {
|
||||
giveName string
|
||||
wantShorthand string
|
||||
wantDefault string
|
||||
}{
|
||||
{giveName: "verbose", wantShorthand: "v", wantDefault: "false"},
|
||||
{giveName: "debug", wantShorthand: "", wantDefault: "false"},
|
||||
{giveName: "log-json", wantShorthand: "", wantDefault: "false"},
|
||||
{giveName: "config-file", wantShorthand: "c", wantDefault: "./error-pages.yml"},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.giveName, func(t *testing.T) {
|
||||
flag := cmd.Flag(tt.giveName)
|
||||
|
||||
if flag == nil {
|
||||
assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.wantShorthand, flag.Shorthand)
|
||||
assert.Equal(t, tt.wantDefault, flag.DefValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuting(t *testing.T) {
|
||||
cmd := cli.NewCommand("unit test")
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
var executed bool
|
||||
|
||||
if cmd.Run == nil { // override "Run" property for test (if it was not set)
|
||||
cmd.Run = func(cmd *cobra.Command, args []string) {
|
||||
executed = true
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.True(t, executed)
|
||||
}
|
148
internal/cli/serve/command.go
Normal file
148
internal/cli/serve/command.go
Normal file
@ -0,0 +1,148 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tarampampam/error-pages/internal/breaker"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
appHttp "github.com/tarampampam/error-pages/internal/http"
|
||||
)
|
||||
|
||||
// NewCommand creates `serve` command.
|
||||
func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command {
|
||||
var (
|
||||
f flags
|
||||
cfg *config.Config
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
Aliases: []string{"s", "server"},
|
||||
Short: "Start HTTP server",
|
||||
PreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if configFile == nil {
|
||||
return errors.New("path to the config file is required for this command")
|
||||
}
|
||||
|
||||
if err := f.overrideUsingEnv(cmd.Flags()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c, err := config.FromYamlFile(*configFile); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err = c.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg = c
|
||||
}
|
||||
|
||||
return f.validate()
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error { return run(ctx, log, f, cfg) },
|
||||
}
|
||||
|
||||
f.init(cmd.Flags())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
const serverShutdownTimeout = 15 * time.Second
|
||||
|
||||
// run current command.
|
||||
func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) error { //nolint:funlen
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
|
||||
oss = breaker.NewOSSignals(ctx) // OS signals listener
|
||||
)
|
||||
|
||||
// subscribe for system signals
|
||||
oss.Subscribe(func(sig os.Signal) {
|
||||
log.Warn("Stopping by OS signal..", zap.String("signal", sig.String()))
|
||||
|
||||
cancel()
|
||||
})
|
||||
|
||||
defer func() {
|
||||
cancel() // call the cancellation function after all
|
||||
oss.Stop() // stop system signals listening
|
||||
}()
|
||||
|
||||
// load templates content
|
||||
templates, loadingErr := cfg.LoadTemplates()
|
||||
if loadingErr != nil {
|
||||
return loadingErr
|
||||
} else if len(templates) == 0 {
|
||||
return errors.New("no loaded templates")
|
||||
}
|
||||
|
||||
if f.template.name != "" && f.template.name != errorpage.UseRandom && f.template.name != errorpage.UseRandomOnEachRequest { //nolint:lll
|
||||
if _, found := templates[f.template.name]; !found {
|
||||
return errors.New("requested nonexistent template: " + f.template.name) // requested unknown template
|
||||
}
|
||||
}
|
||||
|
||||
// burn the error codes map
|
||||
codes := make(map[string]tpl.Annotator)
|
||||
for code, desc := range cfg.Pages {
|
||||
codes[code] = tpl.Annotator{Message: desc.Message, Description: desc.Description}
|
||||
}
|
||||
|
||||
// create HTTP server
|
||||
server := appHttp.NewServer(log)
|
||||
|
||||
// register server routes, middlewares, etc.
|
||||
if err := server.Register(f.template.name, templates, codes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startingErrCh := make(chan error, 1) // channel for server starting error
|
||||
|
||||
// start HTTP server in separate goroutine
|
||||
go func(errCh chan<- error) {
|
||||
defer close(errCh)
|
||||
|
||||
log.Info("Server starting",
|
||||
zap.String("addr", f.listen.ip),
|
||||
zap.Uint16("port", f.listen.port),
|
||||
zap.String("template name", f.template.name),
|
||||
)
|
||||
|
||||
if err := server.Start(f.listen.ip, f.listen.port); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}(startingErrCh)
|
||||
|
||||
// and wait for...
|
||||
select {
|
||||
case err := <-startingErrCh: // ..server starting error
|
||||
return err
|
||||
|
||||
case <-ctx.Done(): // ..or context cancellation
|
||||
log.Info("Gracefully server stopping")
|
||||
|
||||
stoppedAt := time.Now()
|
||||
|
||||
// stop the server using created context above
|
||||
if err := server.Stop(serverShutdownTimeout); err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Error("Server stopping timeout exceeded", zap.Duration("timeout", serverShutdownTimeout))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Server stopped", zap.Duration("stopping duration", time.Since(stoppedAt)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
7
internal/cli/serve/command_test.go
Normal file
7
internal/cli/serve/command_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package serve_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
91
internal/cli/serve/flags.go
Normal file
91
internal/cli/serve/flags.go
Normal file
@ -0,0 +1,91 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tarampampam/error-pages/internal/env"
|
||||
)
|
||||
|
||||
type flags struct {
|
||||
listen struct {
|
||||
ip string
|
||||
port uint16
|
||||
}
|
||||
template struct {
|
||||
name string
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
listenFlagName = "listen"
|
||||
portFlagName = "port"
|
||||
templateNameFlagName = "template-name"
|
||||
)
|
||||
|
||||
func (f *flags) init(flagSet *pflag.FlagSet) {
|
||||
flagSet.StringVarP(
|
||||
&f.listen.ip,
|
||||
listenFlagName, "l",
|
||||
"0.0.0.0",
|
||||
fmt.Sprintf("IP address to listen on [$%s]", env.ListenAddr),
|
||||
)
|
||||
flagSet.Uint16VarP(
|
||||
&f.listen.port,
|
||||
portFlagName, "p",
|
||||
8080, //nolint:gomnd // must be same as default healthcheck `--port` flag value
|
||||
fmt.Sprintf("TCP port number [$%s]", env.ListenPort),
|
||||
)
|
||||
flagSet.StringVarP(
|
||||
&f.template.name,
|
||||
templateNameFlagName, "t",
|
||||
"",
|
||||
fmt.Sprintf(
|
||||
"template name (set \"%s\" to use the randomized or \"%s\" to use the randomized template on each request) [$%s]", //nolint:lll
|
||||
errorpage.UseRandom, errorpage.UseRandomOnEachRequest, env.TemplateName,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) {
|
||||
flagSet.VisitAll(func(flag *pflag.Flag) {
|
||||
// flag was NOT defined using CLI (flags should have maximal priority)
|
||||
if !flag.Changed { //nolint:nestif
|
||||
switch flag.Name {
|
||||
case listenFlagName:
|
||||
if envVar, exists := env.ListenAddr.Lookup(); exists {
|
||||
f.listen.ip = strings.TrimSpace(envVar)
|
||||
}
|
||||
|
||||
case portFlagName:
|
||||
if envVar, exists := env.ListenPort.Lookup(); exists {
|
||||
if p, err := strconv.ParseUint(envVar, 10, 16); err == nil { //nolint:gomnd
|
||||
f.listen.port = uint16(p)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("wrong TCP port environment variable [%s] value", envVar)
|
||||
}
|
||||
}
|
||||
|
||||
case templateNameFlagName:
|
||||
if envVar, exists := env.TemplateName.Lookup(); exists {
|
||||
f.template.name = strings.TrimSpace(envVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (f *flags) validate() error {
|
||||
if net.ParseIP(f.listen.ip) == nil {
|
||||
return fmt.Errorf("wrong IP address [%s] for listening", f.listen.ip)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
24
internal/cli/version/command.go
Normal file
24
internal/cli/version/command.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Package version contains CLI `version` command implementation.
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewCommand creates `version` command.
|
||||
func NewCommand(ver string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Aliases: []string{"v", "ver"},
|
||||
Short: "Display application version",
|
||||
RunE: func(*cobra.Command, []string) (err error) {
|
||||
_, err = fmt.Fprintf(os.Stdout, "app version:\t%s (%s)\n", ver, runtime.Version())
|
||||
|
||||
return
|
||||
},
|
||||
}
|
||||
}
|
30
internal/cli/version/command_test.go
Normal file
30
internal/cli/version/command_test.go
Normal file
@ -0,0 +1,30 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/cli/version"
|
||||
)
|
||||
|
||||
func TestProperties(t *testing.T) {
|
||||
cmd := version.NewCommand("")
|
||||
|
||||
assert.Equal(t, "version", cmd.Use)
|
||||
assert.ElementsMatch(t, []string{"v", "ver"}, cmd.Aliases)
|
||||
assert.NotNil(t, cmd.RunE)
|
||||
}
|
||||
|
||||
func TestCommandRun(t *testing.T) {
|
||||
cmd := version.NewCommand("1.2.3@foobar")
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
output := capturer.CaptureStdout(func() {
|
||||
assert.NoError(t, cmd.Execute())
|
||||
})
|
||||
|
||||
assert.Contains(t, output, "1.2.3@foobar")
|
||||
assert.Contains(t, output, runtime.Version())
|
||||
}
|
116
internal/config/config.go
Normal file
116
internal/config/config.go
Normal file
@ -0,0 +1,116 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/a8m/envsubst"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Templates []struct {
|
||||
Path string `yaml:"path"`
|
||||
Name string `yaml:"name"`
|
||||
Content string `yaml:"content"`
|
||||
} `yaml:"templates"`
|
||||
Pages map[string]struct {
|
||||
Message string `yaml:"message"`
|
||||
Description string `yaml:"description"`
|
||||
} `yaml:"pages"`
|
||||
}
|
||||
|
||||
// Validate the config and return an error if something is wrong.
|
||||
func (c Config) Validate() error {
|
||||
if len(c.Templates) == 0 {
|
||||
return errors.New("empty templates list")
|
||||
} else {
|
||||
for i := 0; i < len(c.Templates); i++ {
|
||||
if c.Templates[i].Name == "" && c.Templates[i].Path == "" {
|
||||
return errors.New("empty path and name with index " + strconv.Itoa(i))
|
||||
}
|
||||
|
||||
if c.Templates[i].Path == "" && c.Templates[i].Content == "" {
|
||||
return errors.New("empty path and template content with index " + strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Pages) == 0 {
|
||||
return errors.New("empty pages list")
|
||||
} else {
|
||||
for code := range c.Pages {
|
||||
if code == "" {
|
||||
return errors.New("empty page code")
|
||||
}
|
||||
|
||||
if strings.ContainsRune(code, ' ') {
|
||||
return errors.New("code should not contain whitespaces")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadTemplates loading templates content from the local files and return it.
|
||||
func (c Config) LoadTemplates() (map[string][]byte, error) {
|
||||
var templates = make(map[string][]byte)
|
||||
|
||||
for i := 0; i < len(c.Templates); i++ {
|
||||
var name string
|
||||
|
||||
if c.Templates[i].Name == "" {
|
||||
basename := filepath.Base(c.Templates[i].Path)
|
||||
name = strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||
} else {
|
||||
name = c.Templates[i].Name
|
||||
}
|
||||
|
||||
var content []byte
|
||||
|
||||
if c.Templates[i].Content == "" {
|
||||
b, err := ioutil.ReadFile(c.Templates[i].Path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot load content for the template "+name)
|
||||
}
|
||||
|
||||
content = b
|
||||
} else {
|
||||
content = []byte(c.Templates[i].Content)
|
||||
}
|
||||
|
||||
templates[name] = content
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
||||
// FromYaml creates new config instance using YAML-structured content.
|
||||
func FromYaml(in []byte) (cfg *Config, err error) {
|
||||
cfg = &Config{}
|
||||
|
||||
in, err = envsubst.Bytes(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = yaml.Unmarshal(in, cfg); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot parse configuration file")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FromYamlFile creates new config instance using YAML file.
|
||||
func FromYamlFile(filepath string) (*Config, error) {
|
||||
bytes, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot read configuration file")
|
||||
}
|
||||
|
||||
return FromYaml(bytes)
|
||||
}
|
315
internal/config/config_test.go
Normal file
315
internal/config/config_test.go
Normal file
@ -0,0 +1,315 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/config"
|
||||
)
|
||||
|
||||
func TestConfig_Validate(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveConfig func() config.Config
|
||||
wantError error
|
||||
}{
|
||||
"valid": {
|
||||
giveConfig: func() config.Config {
|
||||
c := config.Config{}
|
||||
|
||||
c.Templates = []struct {
|
||||
Path string `yaml:"path"`
|
||||
Name string `yaml:"name"`
|
||||
Content string `yaml:"content"`
|
||||
}{
|
||||
{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
c.Pages = map[string]struct {
|
||||
Message string `yaml:"message"`
|
||||
Description string `yaml:"description"`
|
||||
}{
|
||||
"400": {"Bad Request", "The server did not understand the request"},
|
||||
}
|
||||
|
||||
return c
|
||||
},
|
||||
wantError: nil,
|
||||
},
|
||||
"empty templates list": {
|
||||
giveConfig: func() config.Config {
|
||||
return config.Config{}
|
||||
},
|
||||
wantError: errors.New("empty templates list"),
|
||||
},
|
||||
"empty path and name": {
|
||||
giveConfig: func() config.Config {
|
||||
c := config.Config{}
|
||||
|
||||
c.Templates = []struct {
|
||||
Path string `yaml:"path"`
|
||||
Name string `yaml:"name"`
|
||||
Content string `yaml:"content"`
|
||||
}{
|
||||
{
|
||||
Path: "foo",
|
||||
Name: "bar",
|
||||
Content: "baz",
|
||||
},
|
||||
{
|
||||
Path: "",
|
||||
Name: "",
|
||||
Content: "blah",
|
||||
},
|
||||
}
|
||||
|
||||
return c
|
||||
},
|
||||
wantError: errors.New("empty path and name with index 1"),
|
||||
},
|
||||
"empty path and template content": {
|
||||
giveConfig: func() config.Config {
|
||||
c := config.Config{}
|
||||
|
||||
c.Templates = []struct {
|
||||
Path string `yaml:"path"`
|
||||
Name string `yaml:"name"`
|
||||
Content string `yaml:"content"`
|
||||
}{
|
||||
{
|
||||
Path: "foo",
|
||||
Name: "bar",
|
||||
Content: "baz",
|
||||
},
|
||||
{
|
||||
Path: "",
|
||||
Name: "blah",
|
||||
Content: "",
|
||||
},
|
||||
}
|
||||
|
||||
return c
|
||||
},
|
||||
wantError: errors.New("empty path and template content with index 1"),
|
||||
},
|
||||
"empty pages list": {
|
||||
giveConfig: func() config.Config {
|
||||
c := config.Config{}
|
||||
|
||||
c.Templates = []struct {
|
||||
Path string `yaml:"path"`
|
||||
Name string `yaml:"name"`
|
||||
Content string `yaml:"content"`
|
||||
}{
|
||||
{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
c.Pages = map[string]struct {
|
||||
Message string `yaml:"message"`
|
||||
Description string `yaml:"description"`
|
||||
}{}
|
||||
|
||||
return c
|
||||
},
|
||||
wantError: errors.New("empty pages list"),
|
||||
},
|
||||
"empty page code": {
|
||||
giveConfig: func() config.Config {
|
||||
c := config.Config{}
|
||||
|
||||
c.Templates = []struct {
|
||||
Path string `yaml:"path"`
|
||||
Name string `yaml:"name"`
|
||||
Content string `yaml:"content"`
|
||||
}{
|
||||
{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
c.Pages = map[string]struct {
|
||||
Message string `yaml:"message"`
|
||||
Description string `yaml:"description"`
|
||||
}{
|
||||
"": {"foo", "bar"},
|
||||
}
|
||||
|
||||
return c
|
||||
},
|
||||
wantError: errors.New("empty page code"),
|
||||
},
|
||||
"code should not contain whitespaces": {
|
||||
giveConfig: func() config.Config {
|
||||
c := config.Config{}
|
||||
|
||||
c.Templates = []struct {
|
||||
Path string `yaml:"path"`
|
||||
Name string `yaml:"name"`
|
||||
Content string `yaml:"content"`
|
||||
}{
|
||||
{"foo", "bar", "baz"},
|
||||
}
|
||||
|
||||
c.Pages = map[string]struct {
|
||||
Message string `yaml:"message"`
|
||||
Description string `yaml:"description"`
|
||||
}{
|
||||
" 123": {"foo", "bar"},
|
||||
}
|
||||
|
||||
return c
|
||||
},
|
||||
wantError: errors.New("code should not contain whitespaces"),
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := tt.giveConfig().Validate()
|
||||
|
||||
if tt.wantError != nil {
|
||||
assert.EqualError(t, err, tt.wantError.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromYaml(t *testing.T) {
|
||||
var cases = []struct { //nolint:maligned
|
||||
name string
|
||||
giveYaml []byte
|
||||
giveEnv map[string]string
|
||||
wantErr bool
|
||||
checkResultFn func(*testing.T, *config.Config)
|
||||
}{
|
||||
{
|
||||
name: "with all possible values",
|
||||
giveEnv: map[string]string{
|
||||
"__GHOST_PATH": "./templates/ghost.html",
|
||||
"__GHOST_NAME": "ghost",
|
||||
},
|
||||
giveYaml: []byte(`
|
||||
templates:
|
||||
- path: ${__GHOST_PATH}
|
||||
name: ${__GHOST_NAME:-default_value} # name is optional
|
||||
- path: ./templates/l7-light.html
|
||||
- name: Foo
|
||||
content: |
|
||||
Some content
|
||||
New line
|
||||
|
||||
pages:
|
||||
400:
|
||||
message: Bad Request
|
||||
description: The server did not understand the request
|
||||
|
||||
401:
|
||||
message: Unauthorized
|
||||
description: The requested page needs a username and a password
|
||||
`),
|
||||
wantErr: false,
|
||||
checkResultFn: func(t *testing.T, cfg *config.Config) {
|
||||
assert.Len(t, cfg.Templates, 3)
|
||||
assert.Equal(t, "./templates/ghost.html", cfg.Templates[0].Path)
|
||||
assert.Equal(t, "ghost", cfg.Templates[0].Name)
|
||||
assert.Equal(t, "", cfg.Templates[0].Content)
|
||||
assert.Equal(t, "./templates/l7-light.html", cfg.Templates[1].Path)
|
||||
assert.Equal(t, "", cfg.Templates[1].Name)
|
||||
assert.Equal(t, "", cfg.Templates[1].Content)
|
||||
assert.Equal(t, "", cfg.Templates[2].Path)
|
||||
assert.Equal(t, "Foo", cfg.Templates[2].Name)
|
||||
assert.Equal(t, "Some content\nNew line\n", cfg.Templates[2].Content)
|
||||
|
||||
assert.Len(t, cfg.Pages, 2)
|
||||
assert.Equal(t, "Bad Request", cfg.Pages["400"].Message)
|
||||
assert.Equal(t, "The server did not understand the request", cfg.Pages["400"].Description)
|
||||
assert.Equal(t, "Unauthorized", cfg.Pages["401"].Message)
|
||||
assert.Equal(t, "The requested page needs a username and a password", cfg.Pages["401"].Description)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "broken yaml",
|
||||
giveYaml: []byte(`foo bar`),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.giveEnv != nil {
|
||||
for key, value := range tt.giveEnv {
|
||||
assert.NoError(t, os.Setenv(key, value))
|
||||
}
|
||||
}
|
||||
|
||||
conf, err := config.FromYaml(tt.giveYaml)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
tt.checkResultFn(t, conf)
|
||||
}
|
||||
|
||||
if tt.giveEnv != nil {
|
||||
for key := range tt.giveEnv {
|
||||
assert.NoError(t, os.Unsetenv(key))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromYamlFile(t *testing.T) {
|
||||
var cases = []struct { //nolint:maligned
|
||||
name string
|
||||
giveYamlFilePath string
|
||||
wantErr bool
|
||||
checkResultFn func(*testing.T, *config.Config)
|
||||
}{
|
||||
{
|
||||
name: "with all possible values",
|
||||
giveYamlFilePath: "./testdata/simple.yml",
|
||||
wantErr: false,
|
||||
checkResultFn: func(t *testing.T, cfg *config.Config) {
|
||||
assert.Len(t, cfg.Templates, 2)
|
||||
assert.Equal(t, "./templates/ghost.html", cfg.Templates[0].Path)
|
||||
assert.Equal(t, "ghost", cfg.Templates[0].Name)
|
||||
assert.Equal(t, "./templates/l7-light.html", cfg.Templates[1].Path)
|
||||
assert.Equal(t, "", cfg.Templates[1].Name)
|
||||
|
||||
assert.Len(t, cfg.Pages, 2)
|
||||
assert.Equal(t, "Bad Request", cfg.Pages["400"].Message)
|
||||
assert.Equal(t, "The server did not understand the request", cfg.Pages["400"].Description)
|
||||
assert.Equal(t, "Unauthorized", cfg.Pages["401"].Message)
|
||||
assert.Equal(t, "The requested page needs a username and a password", cfg.Pages["401"].Description)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "broken yaml",
|
||||
giveYamlFilePath: "./testdata/broken.yml",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong file path",
|
||||
giveYamlFilePath: "foo bar",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
conf, err := config.FromYamlFile(tt.giveYamlFilePath)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
tt.checkResultFn(t, conf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
1
internal/config/testdata/broken.yml
vendored
Normal file
1
internal/config/testdata/broken.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
foo bar
|
13
internal/config/testdata/simple.yml
vendored
Normal file
13
internal/config/testdata/simple.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
templates:
|
||||
- path: ./templates/ghost.html
|
||||
name: ghost # name is optional
|
||||
- path: ./templates/l7-light.html
|
||||
|
||||
pages:
|
||||
400:
|
||||
message: Bad Request
|
||||
description: The server did not understand the request
|
||||
|
||||
401:
|
||||
message: Unauthorized
|
||||
description: The requested page needs a username and a password
|
21
internal/env/env.go
vendored
Normal file
21
internal/env/env.go
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
// Package env contains all about environment variables, that can be used by current application.
|
||||
package env
|
||||
|
||||
import "os"
|
||||
|
||||
type envVariable string
|
||||
|
||||
const (
|
||||
ListenAddr envVariable = "LISTEN_ADDR" // IP address for listening
|
||||
ListenPort envVariable = "LISTEN_PORT" // port number for listening
|
||||
TemplateName envVariable = "TEMPLATE_NAME" // template name
|
||||
ConfigFilePath envVariable = "CONFIG_FILE" // path to the config file
|
||||
)
|
||||
|
||||
// String returns environment variable name in the string representation.
|
||||
func (e envVariable) String() string { return string(e) }
|
||||
|
||||
// Lookup retrieves the value of the environment variable. If the variable is present in the environment the value
|
||||
// (which may be empty) is returned and the boolean is true. Otherwise the returned value will be empty and the
|
||||
// boolean will be false.
|
||||
func (e envVariable) Lookup() (string, bool) { return os.LookupEnv(string(e)) }
|
45
internal/env/env_test.go
vendored
Normal file
45
internal/env/env_test.go
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
assert.Equal(t, "LISTEN_ADDR", string(ListenAddr))
|
||||
assert.Equal(t, "LISTEN_PORT", string(ListenPort))
|
||||
assert.Equal(t, "TEMPLATE_NAME", string(TemplateName))
|
||||
assert.Equal(t, "CONFIG_FILE", string(ConfigFilePath))
|
||||
}
|
||||
|
||||
func TestEnvVariable_Lookup(t *testing.T) {
|
||||
cases := []struct {
|
||||
giveEnv envVariable
|
||||
}{
|
||||
{giveEnv: ListenAddr},
|
||||
{giveEnv: ListenPort},
|
||||
{giveEnv: TemplateName},
|
||||
{giveEnv: ConfigFilePath},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.giveEnv.String(), func(t *testing.T) {
|
||||
assert.NoError(t, os.Unsetenv(tt.giveEnv.String())) // make sure that env is unset for test
|
||||
|
||||
defer func() { assert.NoError(t, os.Unsetenv(tt.giveEnv.String())) }()
|
||||
|
||||
value, exists := tt.giveEnv.Lookup()
|
||||
assert.False(t, exists)
|
||||
assert.Empty(t, value)
|
||||
|
||||
assert.NoError(t, os.Setenv(tt.giveEnv.String(), "foo"))
|
||||
|
||||
value, exists = tt.giveEnv.Lookup()
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "foo", value)
|
||||
})
|
||||
}
|
||||
}
|
35
internal/http/common/handlers.go
Normal file
35
internal/http/common/handlers.go
Normal file
@ -0,0 +1,35 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
const internalErrorPattern = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>Internal error occurred</title>
|
||||
<style>
|
||||
html,body {background-color: #0e0e0e;color:#fff;font-family:'Nunito',sans-serif;height:100%;margin:0}
|
||||
.message {height:100%;align-items:center;display:flex;justify-content:center;position:relative;font-size:1.4em}
|
||||
img {padding-right: .4em}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="message">
|
||||
<img src="https://hsto.org/webt/fs/sx/gt/fssxgtssfg689qxboqvjil5yz8g.png" alt="logo" height="32">
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
func HandleInternalHTTPError(ctx *fasthttp.RequestCtx, statusCode int, message string) {
|
||||
ctx.SetStatusCode(statusCode)
|
||||
ctx.SetContentType("text/html; charset=UTF-8")
|
||||
|
||||
_, _ = ctx.WriteString(strings.ReplaceAll(internalErrorPattern, "{{ message }}", message))
|
||||
}
|
7
internal/http/common/handlers_test.go
Normal file
7
internal/http/common/handlers_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package common_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
33
internal/http/common/middlewares.go
Normal file
33
internal/http/common/middlewares.go
Normal file
@ -0,0 +1,33 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
var (
|
||||
startedAt = time.Now()
|
||||
ua = string(ctx.UserAgent())
|
||||
)
|
||||
|
||||
h(ctx)
|
||||
|
||||
if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("HTTP request processed",
|
||||
zap.String("useragent", ua),
|
||||
zap.String("method", string(ctx.Method())),
|
||||
zap.String("url", string(ctx.RequestURI())),
|
||||
zap.Int("status_code", ctx.Response.StatusCode()),
|
||||
zap.Bool("connection_close", ctx.Response.ConnectionClose()),
|
||||
zap.Duration("duration", time.Since(startedAt)),
|
||||
)
|
||||
}
|
||||
}
|
7
internal/http/common/middlewares_test.go
Normal file
7
internal/http/common/middlewares_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package common_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing2(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
85
internal/http/handlers/errorpage/handler.go
Normal file
85
internal/http/handlers/errorpage/handler.go
Normal file
@ -0,0 +1,85 @@
|
||||
package errorpage
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tarampampam/error-pages/internal/http/common"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
const (
|
||||
UseRandom = "random"
|
||||
UseRandomOnEachRequest = "i-said-random"
|
||||
)
|
||||
|
||||
// NewHandler creates handler for error pages serving.
|
||||
func NewHandler(
|
||||
templateName string,
|
||||
templates map[string][]byte,
|
||||
codes map[string]tpl.Annotator,
|
||||
) (fasthttp.RequestHandler, error) {
|
||||
if len(templates) == 0 {
|
||||
return nil, errors.New("empty templates map")
|
||||
}
|
||||
|
||||
var (
|
||||
rnd = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec
|
||||
templateNames = templateTames(templates)
|
||||
)
|
||||
|
||||
if templateName == "" { // on empty template name
|
||||
templateName = templateNames[0] // pick the first
|
||||
} else if templateName == UseRandom { // on "random" template name
|
||||
templateName = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
|
||||
}
|
||||
|
||||
if _, found := templates[templateName]; !found && templateName != UseRandomOnEachRequest {
|
||||
return nil, errors.New("wrong template name passed")
|
||||
}
|
||||
|
||||
var pages = tpl.NewErrors(templates, codes)
|
||||
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
var useTemplate = templateName // default
|
||||
|
||||
if templateName == UseRandomOnEachRequest {
|
||||
useTemplate = templateNames[rnd.Intn(len(templateNames))] // pick the randomized
|
||||
}
|
||||
|
||||
if code, ok := ctx.UserValue("code").(string); ok {
|
||||
if content, err := pages.Get(useTemplate, code); err == nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
_, _ = ctx.Write(content)
|
||||
} else {
|
||||
common.HandleInternalHTTPError(
|
||||
ctx,
|
||||
fasthttp.StatusNotFound,
|
||||
"requested code not available: "+err.Error(),
|
||||
)
|
||||
}
|
||||
} else { // will never happen
|
||||
common.HandleInternalHTTPError(
|
||||
ctx,
|
||||
fasthttp.StatusInternalServerError,
|
||||
"cannot extract requested code from the request",
|
||||
)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func templateTames(templates map[string][]byte) []string {
|
||||
var templateNames = make([]string, 0, len(templates))
|
||||
|
||||
for name := range templates {
|
||||
templateNames = append(templateNames, name)
|
||||
}
|
||||
|
||||
sort.Strings(templateNames)
|
||||
|
||||
return templateNames
|
||||
}
|
7
internal/http/handlers/errorpage/handler_test.go
Normal file
7
internal/http/handlers/errorpage/handler_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package errorpage_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
23
internal/http/handlers/healthz/handler.go
Normal file
23
internal/http/handlers/healthz/handler.go
Normal file
@ -0,0 +1,23 @@
|
||||
package healthz
|
||||
|
||||
import "github.com/valyala/fasthttp"
|
||||
|
||||
// checker allows to check some service part.
|
||||
type checker interface {
|
||||
// Check makes a check and return error only if something is wrong.
|
||||
Check() error
|
||||
}
|
||||
|
||||
// NewHandler creates healthcheck handler.
|
||||
func NewHandler(checker checker) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
if err := checker.Check(); err != nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusServiceUnavailable)
|
||||
_, _ = ctx.WriteString(err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
}
|
||||
}
|
7
internal/http/handlers/healthz/handler_test.go
Normal file
7
internal/http/handlers/healthz/handler_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package healthz_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
26
internal/http/handlers/version/handler.go
Normal file
26
internal/http/handlers/version/handler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// NewHandler creates version handler.
|
||||
func NewHandler(ver string) fasthttp.RequestHandler {
|
||||
var cache []byte
|
||||
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
if cache == nil {
|
||||
cache, _ = json.Marshal(struct {
|
||||
Version string `json:"version"`
|
||||
}{
|
||||
Version: ver,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.SetContentType("application/json")
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
_, _ = ctx.Write(cache)
|
||||
}
|
||||
}
|
7
internal/http/handlers/version/handler_test.go
Normal file
7
internal/http/handlers/version/handler_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package version_test
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
106
internal/http/server.go
Normal file
106
internal/http/server.go
Normal file
@ -0,0 +1,106 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fasthttp/router"
|
||||
"github.com/tarampampam/error-pages/internal/checkers"
|
||||
"github.com/tarampampam/error-pages/internal/http/common"
|
||||
errorpageHandler "github.com/tarampampam/error-pages/internal/http/handlers/errorpage"
|
||||
healthzHandler "github.com/tarampampam/error-pages/internal/http/handlers/healthz"
|
||||
versionHandler "github.com/tarampampam/error-pages/internal/http/handlers/version"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
"github.com/tarampampam/error-pages/internal/version"
|
||||
"github.com/valyala/fasthttp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
fast *fasthttp.Server
|
||||
router *router.Router
|
||||
}
|
||||
|
||||
const (
|
||||
defaultWriteTimeout = time.Second * 7
|
||||
defaultReadTimeout = time.Second * 7
|
||||
defaultIdleTimeout = time.Second * 15
|
||||
)
|
||||
|
||||
func NewServer(log *zap.Logger) Server {
|
||||
r := router.New()
|
||||
|
||||
return Server{
|
||||
// fasthttp docs: <https://github.com/valyala/fasthttp>
|
||||
fast: &fasthttp.Server{
|
||||
WriteTimeout: defaultWriteTimeout,
|
||||
ReadTimeout: defaultReadTimeout,
|
||||
IdleTimeout: defaultIdleTimeout,
|
||||
Handler: common.LogRequest(r.Handler, log),
|
||||
NoDefaultServerHeader: true,
|
||||
ReduceMemoryUsage: true,
|
||||
CloseOnShutdown: true,
|
||||
Logger: zap.NewStdLog(log),
|
||||
},
|
||||
router: r,
|
||||
}
|
||||
}
|
||||
|
||||
// Start server.
|
||||
func (s *Server) Start(ip string, port uint16) error {
|
||||
return s.fast.ListenAndServe(ip + ":" + strconv.Itoa(int(port)))
|
||||
}
|
||||
|
||||
// Register server routes, middlewares, etc.
|
||||
// Router docs: <https://github.com/fasthttp/router>
|
||||
func (s *Server) Register(
|
||||
templateName string,
|
||||
templates map[string][]byte,
|
||||
codes map[string]tpl.Annotator,
|
||||
) error {
|
||||
s.router.GET("/", func(ctx *fasthttp.RequestCtx) {
|
||||
common.HandleInternalHTTPError(
|
||||
ctx,
|
||||
fasthttp.StatusNotFound,
|
||||
"Hi there! Error pages are available at the following URLs: /{code}.html",
|
||||
)
|
||||
})
|
||||
|
||||
s.router.NotFound = func(ctx *fasthttp.RequestCtx) {
|
||||
common.HandleInternalHTTPError(
|
||||
ctx,
|
||||
fasthttp.StatusNotFound,
|
||||
"Wrong request URL. Error pages are available at the following URLs: /{code}.html",
|
||||
)
|
||||
}
|
||||
|
||||
s.router.GET("/version", versionHandler.NewHandler(version.Version()))
|
||||
s.router.ANY("/health/live", healthzHandler.NewHandler(checkers.NewLiveChecker()))
|
||||
|
||||
if h, err := errorpageHandler.NewHandler(templateName, templates, codes); err != nil {
|
||||
return err
|
||||
} else {
|
||||
s.router.GET("/{code}.html", h)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop server.
|
||||
func (s *Server) Stop(timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout) // TODO replace with simple time.After
|
||||
defer cancel()
|
||||
|
||||
ch := make(chan error, 1) // channel for server stopping error
|
||||
|
||||
go func() { defer close(ch); ch <- s.fast.Shutdown() }()
|
||||
|
||||
select {
|
||||
case err := <-ch:
|
||||
return err
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
7
internal/http/server_test.go
Normal file
7
internal/http/server_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package http
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNothing(t *testing.T) {
|
||||
t.Skip("tests for this package have not been implemented yet")
|
||||
}
|
38
internal/logger/logger.go
Normal file
38
internal/logger/logger.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Package logger contains functions for a working with application logging.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// New creates new "zap" logger with little customization.
|
||||
func New(verbose, debug, logJSON bool) (*zap.Logger, error) {
|
||||
var config zap.Config
|
||||
|
||||
if logJSON {
|
||||
config = zap.NewProductionConfig()
|
||||
} else {
|
||||
config = zap.NewDevelopmentConfig()
|
||||
config.EncoderConfig.EncodeLevel = zapcore.LowercaseColorLevelEncoder
|
||||
config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05")
|
||||
}
|
||||
|
||||
// default configuration for all encoders
|
||||
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
|
||||
config.Development = false
|
||||
config.DisableStacktrace = true
|
||||
config.DisableCaller = true
|
||||
|
||||
if debug {
|
||||
config.Development = true
|
||||
config.DisableStacktrace = false
|
||||
config.DisableCaller = false
|
||||
}
|
||||
|
||||
if verbose || debug {
|
||||
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
|
||||
}
|
||||
|
||||
return config.Build()
|
||||
}
|
80
internal/logger/logger_test.go
Normal file
80
internal/logger/logger_test.go
Normal file
@ -0,0 +1,80 @@
|
||||
package logger_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kami-zh/go-capturer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/logger"
|
||||
)
|
||||
|
||||
func TestNewNotVerboseDebugJSON(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(false, false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
|
||||
assert.NotContains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewVerboseNotDebugJSON(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(true, false, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\tinf msg`, output)
|
||||
assert.Contains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewVerboseDebugNotJSON(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(true, true, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
assert.Contains(t, output, time.Now().Format("15:04:05"))
|
||||
assert.Regexp(t, `\t.+info.+\t.+logger_test\.go:\d+\tinf msg`, output)
|
||||
assert.Contains(t, output, "dbg msg")
|
||||
assert.Contains(t, output, "err msg")
|
||||
}
|
||||
|
||||
func TestNewNotVerboseDebugButJSON(t *testing.T) {
|
||||
output := capturer.CaptureStderr(func() {
|
||||
log, err := logger.New(false, false, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log.Info("inf msg")
|
||||
log.Debug("dbg msg")
|
||||
log.Error("err msg")
|
||||
})
|
||||
|
||||
// replace timestamp field with fixed value
|
||||
fakeTimestamp := regexp.MustCompile(`"ts":\d+\.\d+,`)
|
||||
output = fakeTimestamp.ReplaceAllString(output, `"ts":0.1,`)
|
||||
|
||||
lines := strings.Split(strings.Trim(output, "\n"), "\n")
|
||||
|
||||
assert.JSONEq(t, `{"level":"info","ts":0.1,"msg":"inf msg"}`, lines[0])
|
||||
assert.JSONEq(t, `{"level":"error","ts":0.1,"msg":"err msg"}`, lines[1])
|
||||
}
|
102
internal/tpl/errors.go
Normal file
102
internal/tpl/errors.go
Normal file
@ -0,0 +1,102 @@
|
||||
package tpl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Annotator allows to annotate error code.
|
||||
type Annotator struct {
|
||||
Message string
|
||||
Description string
|
||||
}
|
||||
|
||||
// Errors is a "cached storage" for the rendered error pages for the different templates and codes.
|
||||
type Errors struct {
|
||||
templates map[string][]byte
|
||||
codes map[string]Annotator
|
||||
|
||||
cacheMu sync.RWMutex
|
||||
cache map[string]map[string][]byte // map[template]map[code]content
|
||||
}
|
||||
|
||||
// NewErrors creates new Errors.
|
||||
func NewErrors(templates map[string][]byte, codes map[string]Annotator) *Errors {
|
||||
return &Errors{
|
||||
templates: templates,
|
||||
codes: codes,
|
||||
cache: make(map[string]map[string][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Errors) existsInCache(template, code string) ([]byte, bool) {
|
||||
e.cacheMu.RLock()
|
||||
defer e.cacheMu.RUnlock()
|
||||
|
||||
if codes, tplOk := e.cache[template]; tplOk {
|
||||
if content, codeOk := codes[code]; codeOk {
|
||||
return content, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (e *Errors) putInCache(template, code string) error {
|
||||
if _, ok := e.templates[template]; !ok {
|
||||
return errors.New("template \"" + template + "\" does not exists")
|
||||
}
|
||||
|
||||
if _, ok := e.codes[code]; !ok {
|
||||
return errors.New("code \"" + code + "\" does not exists")
|
||||
}
|
||||
|
||||
e.cacheMu.Lock()
|
||||
defer e.cacheMu.Unlock()
|
||||
|
||||
if _, ok := e.cache[template]; !ok {
|
||||
e.cache[template] = make(map[string][]byte)
|
||||
}
|
||||
|
||||
e.cache[template][code] = Replace(e.templates[template], Replaces{
|
||||
Code: code,
|
||||
Message: e.codes[code].Message,
|
||||
Description: e.codes[code].Description,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the rendered error page content.
|
||||
func (e *Errors) Get(template, code string) ([]byte, error) {
|
||||
if content, ok := e.existsInCache(template, code); ok {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
if err := e.putInCache(template, code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.cacheMu.RLock()
|
||||
defer e.cacheMu.RUnlock()
|
||||
|
||||
return e.cache[template][code], nil
|
||||
}
|
||||
|
||||
// VisitAll allows to iterate all possible error pages and templates.
|
||||
func (e *Errors) VisitAll(fn func(template, code string, content []byte) error) error {
|
||||
for tpl := range e.templates {
|
||||
for code := range e.codes {
|
||||
content, err := e.Get(tpl, code)
|
||||
if err != nil {
|
||||
return err // will never happen
|
||||
}
|
||||
|
||||
if err = fn(tpl, code, content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
106
internal/tpl/errors_test.go
Normal file
106
internal/tpl/errors_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
package tpl_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
func TestErrors_Get(t *testing.T) {
|
||||
e := tpl.NewErrors(
|
||||
map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
|
||||
map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
|
||||
)
|
||||
|
||||
content, err := e.Get("foo", "200")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "200: ok all is ok", string(content))
|
||||
|
||||
content, err = e.Get("foo", "666")
|
||||
assert.EqualError(t, err, "code \"666\" does not exists")
|
||||
assert.Nil(t, content)
|
||||
|
||||
content, err = e.Get("bar", "200")
|
||||
assert.EqualError(t, err, "template \"bar\" does not exists")
|
||||
assert.Nil(t, content)
|
||||
}
|
||||
|
||||
func TestErrors_GetConcurrent(t *testing.T) {
|
||||
e := tpl.NewErrors(
|
||||
map[string][]byte{"foo": []byte("{{ code }}: {{ message }} {{ description }}")},
|
||||
map[string]tpl.Annotator{"200": {"ok", "all is ok"}},
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 1234; i++ {
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
content, err := e.Get("foo", "200")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "200: ok all is ok", string(content))
|
||||
|
||||
content, err = e.Get("foo", "666")
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, content)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestErrors_VisitAll(t *testing.T) {
|
||||
e := tpl.NewErrors(
|
||||
map[string][]byte{
|
||||
"foo": []byte("{{ code }}: {{ message }} {{ description }}"),
|
||||
"bar": []byte("{{ code }}: {{ message }} {{ description }}"),
|
||||
},
|
||||
map[string]tpl.Annotator{
|
||||
"200": {"ok", "all is ok"},
|
||||
"400": {"Bad Request", "The server did not understand the request"},
|
||||
},
|
||||
)
|
||||
|
||||
visited := make(map[string]map[string]bool) // map[template]codes
|
||||
|
||||
assert.NoError(t, e.VisitAll(func(template, code string, content []byte) error {
|
||||
if _, ok := visited[template]; !ok {
|
||||
visited[template] = make(map[string]bool)
|
||||
}
|
||||
|
||||
visited[template][code] = true
|
||||
|
||||
assert.NotNil(t, content)
|
||||
|
||||
return nil
|
||||
}))
|
||||
|
||||
assert.Len(t, visited, 2)
|
||||
assert.Len(t, visited["foo"], 2)
|
||||
assert.True(t, visited["foo"]["200"])
|
||||
assert.True(t, visited["foo"]["400"])
|
||||
assert.Len(t, visited["bar"], 2)
|
||||
assert.True(t, visited["bar"]["200"])
|
||||
assert.True(t, visited["bar"]["400"])
|
||||
}
|
||||
|
||||
func TestErrors_VisitAllWillReturnTheError(t *testing.T) {
|
||||
e := tpl.NewErrors(
|
||||
map[string][]byte{
|
||||
"foo": []byte("{{ code }}: {{ message }} {{ description }}"),
|
||||
},
|
||||
map[string]tpl.Annotator{
|
||||
"200": {"ok", "all is ok"},
|
||||
},
|
||||
)
|
||||
|
||||
assert.EqualError(t, e.VisitAll(func(template, code string, content []byte) error {
|
||||
return errors.New("foo error")
|
||||
}), "foo error")
|
||||
}
|
47
internal/tpl/replace.go
Normal file
47
internal/tpl/replace.go
Normal file
@ -0,0 +1,47 @@
|
||||
package tpl
|
||||
|
||||
import "bytes"
|
||||
|
||||
type Replaces struct {
|
||||
Code string
|
||||
Message string
|
||||
Description string
|
||||
}
|
||||
|
||||
const (
|
||||
tknCode byte = iota + 1
|
||||
tknMessage
|
||||
tknDescription
|
||||
)
|
||||
|
||||
var tknSets = map[byte][][]byte{ //nolint:gochecknoglobals
|
||||
tknCode: {[]byte("{{code}}"), []byte("{{ code }}")},
|
||||
tknMessage: {[]byte("{{message}}"), []byte("{{ message }}")},
|
||||
tknDescription: {[]byte("{{description}}"), []byte("{{ description }}")},
|
||||
}
|
||||
|
||||
// Replace found tokens in the incoming slice with passed tokens.
|
||||
func Replace(in []byte, re Replaces) []byte {
|
||||
for tkn, set := range tknSets {
|
||||
var replaceWith []byte
|
||||
|
||||
switch tkn {
|
||||
case tknCode:
|
||||
replaceWith = []byte(re.Code)
|
||||
case tknMessage:
|
||||
replaceWith = []byte(re.Message)
|
||||
case tknDescription:
|
||||
replaceWith = []byte(re.Description)
|
||||
default:
|
||||
panic("tpl: unsupported token")
|
||||
}
|
||||
|
||||
if len(replaceWith) > 0 {
|
||||
for i := 0; i < len(set); i++ {
|
||||
in = bytes.ReplaceAll(in, set[i], replaceWith)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return in
|
||||
}
|
66
internal/tpl/replace_test.go
Normal file
66
internal/tpl/replace_test.go
Normal file
@ -0,0 +1,66 @@
|
||||
package tpl_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tarampampam/error-pages/internal/tpl"
|
||||
)
|
||||
|
||||
func ExampleReplace() {
|
||||
var in = []byte("{{ code }}: {{message}} ({{ description }})")
|
||||
|
||||
fmt.Println(string(tpl.Replace(in, tpl.Replaces{
|
||||
Code: "400",
|
||||
Message: "Bad Request",
|
||||
Description: "The server did not understand the request",
|
||||
})))
|
||||
|
||||
// Output:
|
||||
// 400: Bad Request (The server did not understand the request)
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
for name, tt := range map[string]struct {
|
||||
giveIn []byte
|
||||
giveRe tpl.Replaces
|
||||
wantResult []byte
|
||||
}{
|
||||
"common": {
|
||||
giveIn: []byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "),
|
||||
giveRe: tpl.Replaces{
|
||||
Code: "123",
|
||||
Message: "message",
|
||||
Description: "desc",
|
||||
},
|
||||
wantResult: []byte("-- 123 123 __ message desc "),
|
||||
},
|
||||
"alpha and underline in the code": {
|
||||
giveIn: []byte("\t{{ code }}\t"),
|
||||
giveRe: tpl.Replaces{
|
||||
Code: " qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ ",
|
||||
},
|
||||
wantResult: []byte("\t qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_ \t"),
|
||||
},
|
||||
} {
|
||||
tt := tt
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.wantResult, tpl.Replace(tt.giveIn, tt.giveRe))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReplace(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
tpl.Replace([]byte("-- {{ code }} {{code}} __ {{message}} {{ description }} "), tpl.Replaces{
|
||||
Code: "123",
|
||||
Message: "message",
|
||||
Description: "desc",
|
||||
})
|
||||
}
|
||||
}
|
18
internal/version/version.go
Normal file
18
internal/version/version.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Package version is used as a place, where application version defined.
|
||||
package version
|
||||
|
||||
import "strings"
|
||||
|
||||
// version value will be set during compilation.
|
||||
var version = "v0.0.0@undefined"
|
||||
|
||||
// Version returns version value (without `v` prefix).
|
||||
func Version() string {
|
||||
v := strings.TrimSpace(version)
|
||||
|
||||
if len(v) > 1 && ((v[0] == 'v' || v[0] == 'V') && (v[1] >= '0' && v[1] <= '9')) {
|
||||
return v[1:]
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
38
internal/version/version_test.go
Normal file
38
internal/version/version_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
for give, want := range map[string]string{
|
||||
// without changes
|
||||
"vvv": "vvv",
|
||||
"victory": "victory",
|
||||
"voodoo": "voodoo",
|
||||
"foo": "foo",
|
||||
"0.0.0": "0.0.0",
|
||||
"v": "v",
|
||||
"V": "V",
|
||||
|
||||
// "v" prefix removal
|
||||
"v0.0.0": "0.0.0",
|
||||
"V0.0.0": "0.0.0",
|
||||
"v1": "1",
|
||||
"V1": "1",
|
||||
|
||||
// with spaces
|
||||
" 0.0.0": "0.0.0",
|
||||
"v0.0.0 ": "0.0.0",
|
||||
" V0.0.0": "0.0.0",
|
||||
"v1 ": "1",
|
||||
" V1": "1",
|
||||
"v ": "v",
|
||||
} {
|
||||
version = give
|
||||
|
||||
if v := Version(); v != want {
|
||||
t.Errorf("want: %s, got: %s", want, v)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user