Merge branch 'master' into feature/tls-renegotiation

pull/482/head
Anže Jenšterle 2020-04-25 00:37:00 +02:00 committed by GitHub
commit 23e8b9f8db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 3645 additions and 7257 deletions

19
.github/stale.yml vendored
View File

@ -1,19 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- bug
- urgent
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions. If this is still an problem, please create a new issue.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: true

162
.github/workflows/dev.yml vendored Normal file
View File

@ -0,0 +1,162 @@
name: Dev Release
on:
push:
branches:
- dev
jobs:
compile:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: '1.14.x'
- name: Add GOBIN to PATH
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Install Global Dependencies
run: npm install -g yarn sass cross-env
- name: Checkout Statping Repo
uses: actions/checkout@v2
- uses: actions/cache@v1
id: nodecache
with:
path: ./frontend/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Download Frontend Dependencies
if: steps.nodecache.outputs.cache-hit != 'true'
working-directory: ./frontend
run: yarn
- uses: actions/cache@v1
id: golangcache
with:
path: |
~/go/pkg/mod
~/go/bin
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download Go mods
if: steps.golangcache.outputs.cache-hit != 'true'
run: |
go mod download
go mod verify
make test-deps
- name: Build Frontend Statping
run: make clean frontend-build
- name: Upload Compiled Frontend
uses: actions/upload-artifact@v1
with:
name: static-frontend
path: /home/runner/work/statping/statping/source/dist
test:
needs: compile
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
uses: actions/checkout@v2
- name: Install Go
uses: actions/setup-go@v1
with:
go-version: '1.14.x'
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Install Global Dependencies
run: npm install -g yarn sass newman cross-env wait-on @sentry/cli
- uses: actions/cache@v1
id: golangcache
with:
path: |
~/go/pkg/mod
~/go/bin
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Download Go mods
if: steps.golangcache.outputs.cache-hit != 'true'
run: |
go mod download
go mod verify
make test-deps
- name: Download Compiled Frontend
uses: actions/download-artifact@v1
with:
name: static-frontend
path: /home/runner/work/statping/statping/source/dist
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Go Tests
run: SASS=`which sass` go test -v -covermode=count -coverprofile=coverage.out -p=1 ./...
env:
VERSION: ${{ env.VERSION }}
DB_CONN: sqlite3
STATPING_DIR: /home/runner/work/statping/statping
API_KEY: demopassword123
DISABLE_LOGS: true
ALLOW_REPORTS: true
COVERALLS: ${{ secrets.COVERALLS }}
DISCORD_URL: ${{ secrets.DISCORD_URL }}
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
EMAIL_USER: ${{ secrets.EMAIL_USER }}
EMAIL_PASS: ${{ secrets.EMAIL_PASS }}
EMAIL_OUTGOING: ${{ secrets.EMAIL_OUTGOING }}
EMAIL_SEND_TO: ${{ secrets.EMAIL_SEND_TO }}
EMAIL_PORT: ${{ secrets.EMAIL_PORT }}
MOBILE_ID: ${{ secrets.MOBILE_ID }}
MOBILE_NUMBER: ${{ secrets.MOBILE_NUMBER }}
PUSHOVER_TOKEN: ${{ secrets.PUSHOVER_TOKEN }}
PUSHOVER_API: ${{ secrets.PUSHOVER_API }}
SLACK_URL: ${{ secrets.SLACK_URL }}
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHANNEL: ${{ secrets.TELEGRAM_CHANNEL }}
TWILIO_SID: ${{ secrets.TWILIO_SID }}
TWILIO_SECRET: ${{ secrets.TWILIO_SECRET }}
TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
TWILIO_TO: ${{ secrets.TWILIO_TO }}
- name: Coveralls Testing Coverage
run: goveralls -coverprofile=coverage.out -repotoken $COVERALLS
env:
COVERALLS: ${{ secrets.COVERALLS }}

503
.github/workflows/master.yml vendored Normal file
View File

@ -0,0 +1,503 @@
name: Master Release
on:
push:
branches:
- master
paths-ignore:
- '**.md'
jobs:
compile:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
- name: Add GOBIN to PATH
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Install Global Dependencies
run: npm install -g yarn sass cross-env
- name: Download Frontend Dependencies
if: steps.nodecache.outputs.cache-hit != 'true'
working-directory: ./frontend
run: yarn
- name: Download Go mods
if: steps.golangcache.outputs.cache-hit != 'true'
run: |
go mod download
go mod verify
make test-deps
- name: Build Frontend Statping
run: make clean compile
- name: Upload Compiled Frontend (rice-box.go)
uses: actions/upload-artifact@v1
with:
name: static-rice-box
path: ./source
test:
needs: compile
runs-on: ubuntu-latest
services:
postgres:
image: postgres:10.8
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: password123
POSTGRES_DB: statping
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: password123
MYSQL_DATABASE: statping
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
- uses: actions/setup-node@v1
with:
node-version: '10.x'
- uses: actions/checkout@v2
- name: Install Global Dependencies
run: npm install -g yarn sass newman cross-env wait-on @sentry/cli
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Download Compiled Frontend (rice-box.go)
uses: actions/download-artifact@v1
with:
name: static-rice-box
path: ./source
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Go Tests
run: |
go get gotest.tools/gotestsum
gotestsum --no-summary=skipped --format dots -- -covermode=count -coverprofile=coverage.out -p=1 ./...
env:
VERSION: ${{ env.VERSION }}
DB_CONN: sqlite3
STATPING_DIR: ${{ github.workspace }}
API_KEY: demopassword123
DISABLE_LOGS: true
ALLOW_REPORTS: true
COVERALLS: ${{ secrets.COVERALLS }}
DISCORD_URL: ${{ secrets.DISCORD_URL }}
EMAIL_HOST: ${{ secrets.EMAIL_HOST }}
EMAIL_USER: ${{ secrets.EMAIL_USER }}
EMAIL_PASS: ${{ secrets.EMAIL_PASS }}
EMAIL_OUTGOING: ${{ secrets.EMAIL_OUTGOING }}
EMAIL_SEND_TO: ${{ secrets.EMAIL_SEND_TO }}
EMAIL_PORT: ${{ secrets.EMAIL_PORT }}
MOBILE_ID: ${{ secrets.MOBILE_ID }}
MOBILE_NUMBER: ${{ secrets.MOBILE_NUMBER }}
PUSHOVER_TOKEN: ${{ secrets.PUSHOVER_TOKEN }}
PUSHOVER_API: ${{ secrets.PUSHOVER_API }}
SLACK_URL: ${{ secrets.SLACK_URL }}
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHANNEL: ${{ secrets.TELEGRAM_CHANNEL }}
TWILIO_SID: ${{ secrets.TWILIO_SID }}
TWILIO_SECRET: ${{ secrets.TWILIO_SECRET }}
TWILIO_FROM: ${{ secrets.TWILIO_FROM }}
TWILIO_TO: ${{ secrets.TWILIO_TO }}
- name: Coveralls Testing Coverage
run: |
go get github.com/mattn/goveralls
goveralls -coverprofile=coverage.out -repotoken $COVERALLS
env:
COVERALLS: ${{ secrets.COVERALLS }}
test-postman:
needs: compile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Download Compiled Frontend (rice-box.go)
uses: actions/download-artifact@v1
with:
name: static-rice-box
path: ./source
- name: Install Statping
env:
VERSION: ${{ env.VERSION }}
run: |
make build
chmod +x statping
mv statping $(go env GOPATH)/bin/
- name: Run Statping
run: |
API_SECRET=demosecret123 statping --port=8080 > /dev/null &
sleep 3
- name: Postman Tests
uses: matt-ball/newman-action@master
with:
postmanApiKey: ${{ secrets.POSTMAN_API }}
collection: ./dev/postman.json
environment: ./dev/postman_environment.json
timeoutRequest: 15000
delayRequest: 1000
build-mac:
needs: compile
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
- uses: actions/checkout@v2
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Download Go mods
if: steps.golangcache.outputs.cache-hit != 'true'
run: |
go mod download
go mod verify
make test-deps
- name: Download Compiled Frontend (rice-box.go)
uses: actions/download-artifact@v1
with:
name: static-rice-box
path: ./source
- name: Build Binaries
env:
VERSION: ${{ env.VERSION }}
COMMIT: $GITHUB_SHA
run: make build-mac
- name: Upload MacOSX Builds
uses: actions/upload-artifact@v1
with:
name: darwin-builds
path: ./build
build-linux:
needs: compile
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
- uses: actions/checkout@v2
- name: Install Libraries
run: |
sudo apt update
sudo apt install libc-dev gcc-multilib build-essential musl-dev gcc g++ -y
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Download Go mods
if: steps.golangcache.outputs.cache-hit != 'true'
run: |
go mod download
go mod verify
make test-deps
- name: Download Compiled Frontend (rice-box.go)
uses: actions/download-artifact@v1
with:
name: static-rice-box
path: ./source
- name: Build Binaries
env:
VERSION: ${{ env.VERSION }}
COMMIT: $GITHUB_SHA
run: |
go env
make build-linux
- name: Upload Linux Builds
uses: actions/upload-artifact@v1
with:
name: linux-builds
path: ./build
build-windows:
needs: compile
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: '1.14.2'
- uses: actions/checkout@v2
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Download Go mods
if: steps.golangcache.outputs.cache-hit != 'true'
run: |
go mod download
go mod verify
make test-deps
- name: Download Compiled Frontend (rice-box.go)
uses: actions/download-artifact@v1
with:
name: static-rice-box
path: ./source
- name: Build Binaries
env:
VERSION: ${{ env.VERSION }}
COMMIT: $GITHUB_SHA
run: make build-win
- name: Upload Windows Builds
uses: actions/upload-artifact@v1
with:
name: windows-builds
path: ./build
upload-release:
needs: [test, test-postman, build-linux, build-mac, build-windows]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setting ENV's
run: |
echo "::add-path::$(go env GOPATH)/bin"
echo "::add-path::/opt/hostedtoolcache/node/10.20.1/x64/bin"
echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Download Linux Builds
uses: actions/download-artifact@v1
with:
name: linux-builds
path: ./linux
- name: Download MacOSX Builds
uses: actions/download-artifact@v1
with:
name: darwin-builds
path: ./darwin
- name: Download Windows Builds
uses: actions/download-artifact@v1
with:
name: windows-builds
path: ./windows
- name: Upload Linux Release
id: upload-linux-asset
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ env.VERSION }}
with:
tag_name: v${{ env.VERSION }}
draft: false
prerelease: false
files: |
linux/statping-freebsd-386.tar.gz
linux/statping-freebsd-amd64.tar.gz
linux/statping-freebsd-arm.tar.gz
linux/statping-linux-386.tar.gz
linux/statping-linux-amd64.tar.gz
linux/statping-linux-arm.tar.gz
linux/statping-linux-arm64.tar.gz
linux/statping-openbsd-386.tar.gz
linux/statping-openbsd-amd64.tar.gz
linux/statping-openbsd-arm.tar.gz
linux/statping-openbsd-arm64.tar.gz
- name: Upload MaxOSX Release
id: upload-darwin-asset
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ env.VERSION }}
with:
tag_name: v${{ env.VERSION }}
draft: false
prerelease: false
files: |
darwin/statping-darwin-386.tar.gz
darwin/statping-darwin-amd64.tar.gz
- name: Upload Windows Release
id: upload-windows-asset
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ env.VERSION }}
with:
tag_name: v${{ env.VERSION }}
draft: false
prerelease: false
files: |
windows/statping-windows-386.zip
windows/statping-windows-amd64.zip
windows/statping-windows-arm.zip
docker-release:
needs: upload-release
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
uses: actions/checkout@v2
- name: Setting ENV's
run: echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Base Docker Image
uses: elgohr/Publish-Docker-Github-Action@master
with:
name: statping/statping
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile.base
tags: "base"
- name: Latest/Version Docker Image
uses: elgohr/Publish-Docker-Github-Action@master
env:
VERSION: ${{ env.VERSION }}
ARCH: amd64
DOCKER_CLI_EXPERIMENTAL: enabled
with:
name: statping/statping
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
tags: "latest,v${{ env.VERSION }}"
buildargs: VERSION,ARCH
sentry-release:
needs: upload-release
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
uses: actions/checkout@v2
- name: Setting ENV's
run: echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Sentry Backend Release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: statping
SENTRY_PROJECT: backend
with:
tagName: v${{ env.VERSION }}
environment: production
- name: Sentry Frontend Release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: statping
SENTRY_PROJECT: frontend
with:
tagName: v${{ env.VERSION }}
environment: production
homebrew-release:
needs: upload-release
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
uses: actions/checkout@v2
- name: Setting ENV's
run: echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Update Homebrew Package
env:
VERSION: ${{ env.VERSION }}
TRAVIS_API: ${{ secrets.TRAVIS_API }}
run: make publish-homebrew
slack-update:
needs: upload-release
runs-on: ubuntu-latest
steps:
- name: Checkout Statping Repo
uses: actions/checkout@v2
- name: Setting ENV's
run: echo ::set-env name=VERSION::$(cat version.txt)
shell: bash
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2.0.0
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_URL }}
SLACK_CHANNEL: dev
SLACK_USERNAME: StatpingDev

View File

@ -1,22 +0,0 @@
name: ReleaseWorkflow
on:
release:
types: [published, prereleased]
jobs:
createSentryRelease:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Create a Sentry release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: statping
SENTRY_PROJECT: backend
with:
tagName: ${{ github.ref }}
environment: qa

View File

@ -1,22 +0,0 @@
name: ReleaseFrontendWorkflow
on:
release:
types: [published, prereleased]
jobs:
createSentryRelease:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Create a Sentry Frontend release
uses: tclindner/sentry-releases-action@v1.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_FRONTEND_AUTH_TOKEN }}
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_ORG: statping
SENTRY_PROJECT: frontend
with:
tagName: ${{ github.ref }}
environment: qa

View File

@ -1,14 +0,0 @@
on: push
name: Slack Notification
jobs:
slackNotification:
name: Slack Notification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Slack Notification
uses: rtCamp/action-slack-notify@v2.0.0
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_URL }}
SLACK_CHANNEL: dev
SLACK_USERNAME: Github

17
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Issues
on:
schedule:
- cron: "0 0 * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- name: Stale Issues
uses: actions/stale@v1.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue hasn't had any updates in a while. If this is still a problem, please create a new issue."
days-before-stale: 30
days-before-close: 5
exempt-issue-labels: "bug,urgent,feature,pinned,locked"

View File

@ -1,52 +0,0 @@
after_success:
- "if [[ \"$TRAVIS_BRANCH\" == \"master\" && \"$TRAVIS_PULL_REQUEST\" = \"false\" ]]; then make travis-build; fi"
before_install:
- "rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install stable"
- "nvm install 10.17.0"
- "nvm use 10.17.0 --default"
before_script:
- "mysql -e 'CREATE DATABASE IF NOT EXISTS test;'"
- "psql -c 'create database test;' -U postgres"
branches:
only:
- master
- dev
env:
global:
- "PATH=$HOME/.local/bin:$PATH"
- DB_HOST=localhost
- DB_USER=travis
- DB_PASS=
- DB_DATABASE=test
- GO_ENV=test
- STATPING_DIR=$GOPATH/src/github.com/statping/statping
go: 1.14
go_import_path: github.com/statping/statping
install:
- "npm install -g sass newman cross-env wait-on @sentry/cli"
- "pip install --user awscli"
- "go get github.com/mattn/goveralls"
- "go mod download"
- "go mod verify"
- "make test-deps yarn clean compile install"
language: go
addons:
apt:
packages:
- libgconf-2-4
matrix:
allow_failures:
- go: master
fast_finish: true
notifications:
email: true
os:
- linux
script:
- "travis_retry make clean test-ci"
- "if [[ \"$TRAVIS_BRANCH\" == \"master\" && \"$TRAVIS_PULL_REQUEST\" = \"false\" ]]; then make coverage; fi"
services:
- docker
- postgresql
- mysql
- mongodb

View File

@ -1,3 +1,40 @@
# 0.90.33 (04-24-2020)
- Fixed config loading method
# 0.90.32 (04-23-2020)
- Modified the saving and loading process config.yml
# 0.90.31 (04-21-2020)
- Version bump for github actions
# 0.90.30 (04-19-2020)
- Attempt to fix Github Actions build process
- Fix for empty database connection string, and not starting in setup mode
# 0.90.29 (04-19-2020)
- Added HTTP Redirects for services
- Removed use of SASS environment variable, now finds path or sends error
- Modified Makefile to create new snapcraft versions
- Fixed issue when logs are not initiated yet. Issue #502
- Fixed issue when SQLite (statping.db) is not found Issue #499
- Modified port flag in Docker image
- Fixed issue on startup without config.yml file not starting in setup mode
# 0.90.28 (04-16-2020)
- Fixed postgres timestamp grouping
- Added postman (newman) API testing
- Added Viper and Cobra config/env parsing package
- Added more golang tests
- Modified handlers to use a more generic find method
- Added 'env' command to show variables used in config
- Added 'reset' command that will delete files and backup .db file for a fresh install
- Added error type that has common errors with http status code based on error
# 0.90.27 (04-15-2020)
- Fixed postgres database table creation process
- Modified go build process, additional ARCHs
- Added 'SAMPLE_DATA' environment variable to disable example data on startup. (default: true)
# 0.90.26 (04-13-2020)
- Fixed Delete Failures button/function
- Removed timezone field from Settings (core)

View File

@ -18,5 +18,4 @@ EXPOSE $PORT
HEALTHCHECK --interval=60s --timeout=10s --retries=3 CMD curl -s "http://localhost:$PORT/health" | jq -r -e ".online==true"
CMD statping -port $PORT
CMD statping --port $PORT

View File

@ -10,7 +10,7 @@ PUBLISH_BODY='{ "request": { "branch": "master", "message": "Homebrew update ver
TRAVIS_BUILD_CMD='{ "request": { "branch": "master", "message": "Compile master for Statping v${VERSION}", "config": { "merge_mode": "replace", "language": "go", "go": 1.14, "install": true, "sudo": "required", "services": ["docker"], "env": { "secure": "${TRVIS_SECRET}" }, "before_deploy": ["git config --local user.name \"hunterlong\"", "git config --local user.email \"info@socialeck.com\"", "git tag v$(VERSION) --force"], "deploy": [{ "provider": "releases", "api_key": "$$GITHUB_TOKEN", "file_glob": true, "file": "build/*", "skip_cleanup": true, "on": { "branch": "master" } }], "before_script": ["rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install stable", "nvm install 10.17.0", "nvm use 10.17.0 --default", "npm install -g sass yarn cross-env", "pip install --user awscli"], "script": ["make release"], "after_success": [], "after_deploy": ["make post-release"] } } }'
TEST_DIR=$(GOPATH)/src/github.com/statping/statping
PATH:=/usr/local/bin:$(GOPATH)/bin:$(PATH)
OS = darwin freebsd linux openbsd
OS = freebsd linux openbsd
ARCHS = 386 arm amd64 arm64
all: clean yarn-install compile docker-base docker-vue build-all
@ -39,7 +39,7 @@ release: test-deps
make build-all
test-ci: clean compile test-deps
SASS=`which sass` go test -v -covermode=count -coverprofile=coverage.out -p=1 ./...
DB_CONN=sqlite go test -v -covermode=count -coverprofile=coverage.out -p=1 ./...
goveralls -coverprofile=coverage.out -service=travis-ci -repotoken ${COVERALLS}
cypress: clean
@ -85,7 +85,7 @@ db-up:
docker-compose -f dev/docker-compose.db.yml up -d --remove-orphans
db-down:
docker-compose -f dev/docker-compose.full.yml down --remove-orphans
docker-compose -f dev/docker-compose.db.yml down --volumes --remove-orphans
console:
docker exec -t -i statping /bin/sh
@ -151,30 +151,44 @@ install-local: build
generate:
cd source && go generate
build-bin:
mkdir build
build-linux:
mkdir build || true
export PWD=`pwd`
@for arch in $(ARCHS);\
do \
for os in $(OS);\
do \
echo "Building $$os-$$arch"; \
echo "Building v$$VERSION for $$os-$$arch"; \
mkdir -p releases/statping-$$os-$$arch/; \
GO111MODULE="on" GOOS=$$os GOARCH=$$arch go build -a -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" -o releases/statping-$$os-$$arch/statping ${PWD}/cmd; \
chmod +x releases/statping-$$os-$$arch/statping; \
tar -czf releases/statping-$$os-$$arch.tar.gz -C releases/statping-$$os-$$arch statping; \
GO111MODULE="on" GOOS=$$os GOARCH=$$arch go build -a -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" -o releases/statping-$$os-$$arch/statping ${PWD}/cmd || true; \
chmod +x releases/statping-$$os-$$arch/statping || true; \
tar -czf releases/statping-$$os-$$arch.tar.gz -C releases/statping-$$os-$$arch statping || true; \
done \
done
find ./releases/ -name "*.tar.gz" -type f -size +1M -exec mv "{}" build/ \;
build-win:
build-mac:
mkdir build || true
export PWD=`pwd`
@for arch in $(ARCHS);\
do \
echo "Building windows-$$arch"; \
echo "Building v$$VERSION for darwin-$$arch"; \
mkdir -p releases/statping-darwin-$$arch/; \
GO111MODULE="on" GOOS=darwin GOARCH=$$arch go build -a -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" -o releases/statping-darwin-$$arch/statping ${PWD}/cmd || true; \
chmod +x releases/statping-darwin-$$arch/statping || true; \
tar -czf releases/statping-darwin-$$arch.tar.gz -C releases/statping-darwin-$$arch statping || true; \
done
find ./releases/ -name "*.tar.gz" -type f -size +1M -exec mv "{}" build/ \;
build-win:
mkdir build || true
export PWD=`pwd`
@for arch in $(ARCHS);\
do \
echo "Building v$$VERSION for windows-$$arch"; \
mkdir -p releases/statping-windows-$$arch/; \
GO111MODULE="on" GOOS=windows GOARCH=$$arch go build -a -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" -o releases/statping-windows-$$arch/statping.exe ${PWD}/cmd; \
chmod +x releases/statping-windows-$$arch/statping.exe; \
GO111MODULE="on" GOOS=windows GOARCH=$$arch go build -a -ldflags "-X main.VERSION=${VERSION} -X main.COMMIT=$(TRAVIS_COMMIT)" -o releases/statping-windows-$$arch/statping.exe ${PWD}/cmd || true; \
chmod +x releases/statping-windows-$$arch/statping.exe || true; \
zip -j releases/statping-windows-$$arch.zip releases/statping-windows-$$arch/statping.exe || true; \
done
find ./releases/ -name "*.zip" -type f -size +1M -exec mv "{}" build/ \;
@ -193,15 +207,14 @@ clean:
rm -rf frontend/{logs,plugins,*.db,config.yml,.sass-cache,*.log}
rm -rf dev/{logs,assets,plugins,*.db,config.yml,.sass-cache,*.log,test/app,plugin/*.so}
rm -rf {parts,prime,snap,stage}
rm -rf dev/test/cypress/videos
rm -rf frontend/cypress/videos
rm -f coverage.* sass
rm -f source/rice-box.go
rm -rf **/*.db-journal
rm -rf *.snap
find . -name "*.out" -type f -delete
find . -name "*.cpu" -type f -delete
find . -name "*.mem" -type f -delete
rm -rf {build,releases,tmp}
rm -rf {build,releases,tmp,source/build,snap}
print_details:
@echo \==== Statping Development Instance ====
@ -219,7 +232,7 @@ print_details:
@echo \==== Monitoring and IDE ====
@echo \Grafana: http://localhost:3000 \(username: admin, password: admin\)
build-all: clean compile build-bin build-win
build-all: clean compile build-linux build-mac build-win
coverage: test-deps
$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis -repotoken $(COVERALLS)
@ -232,22 +245,24 @@ download-key:
wget -O statping.gpg $(SIGN_URL)
gpg --import statping.gpg
# build :latest docker tag
docker-build-latest:
docker build --build-arg VERSION=${VERSION} -t statping/statping:latest --no-cache -f Dockerfile .
docker tag statping/statping:latest statping/statping:v${VERSION}
# push the :dev docker tag using curl
publish-dev:
curl -H "Content-Type: application/json" --data '{"docker_tag": "dev"}' -X POST $(DOCKER)
dockerhub-dev:
docker build --build-arg VERSION=${VERSION} -t statping/statping:dev --no-cache -f Dockerfile.base .
docker push statping/statping:dev
publish-latest: publish-base
curl -H "Content-Type: application/json" --data '{"docker_tag": "latest"}' -X POST $(DOCKER)
dockerhub:
docker build --build-arg VERSION=${VERSION} -t statping/statping:base --no-cache -f Dockerfile.base .
docker build --build-arg VERSION=${VERSION} -t statping/statping:latest --no-cache -f Dockerfile .
docker tag statping/statping statping/statping:v${VERSION}
docker push statping/statping:base
docker push statping/statping:v${VERSION}
docker push statping/statping
publish-base:
curl -H "Content-Type: application/json" --data '{"docker_tag": "base"}' -X POST $(DOCKER)
docker-build-dev:
docker build --build-arg VERSION=${VERSION} -t hunterlong/statping:latest --no-cache -f Dockerfile .
docker tag hunterlong/statping:dev hunterlong/statping:dev-v${VERSION}
post-release: frontend-build upload_to_s3 publish-homebrew publish-latest
post-release: frontend-build upload_to_s3 publish-homebrew dockerhub
# update the homebrew application to latest for mac
publish-homebrew:
@ -274,20 +289,28 @@ sentry-release:
sentry-cli releases set-commits --auto v${VERSION}
sentry-cli releases finalize v${VERSION}
snapcraft: clean compile build-bin
snapcraft: clean compile build-linux
mkdir snap
mv snapcraft.yaml snap/
PWD=$(shell pwd)
snapcraft clean statping -s pull
snapcraft clean statping
docker run --rm -v ${PWD}/build/statping-linux-amd64.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=amd64"
snapcraft clean statping -s pull
snapcraft clean statping
docker run --rm -v ${PWD}/build/statping-linux-386.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=i386"
snapcraft clean statping -s pull
snapcraft clean statping
docker run --rm -v ${PWD}/build/statping-linux-arm64.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=arm64"
snapcraft clean statping -s pull
docker run --rm -v ${PWD}/build/statping-linux-arm.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=armhf"
snapcraft clean statping
docker run --rm -v ${PWD}/build/statping-linux-arm.tar.gz:/build/statping-linux.tar.gz -w /build --env VERSION=${VERSION} snapcore/snapcraft bash -c "apt update && snapcraft --target-arch=arm"
snapcraft push statping_${VERSION}_amd64.snap --release stable
snapcraft push statping_${VERSION}_arm64.snap --release stable
snapcraft push statping_${VERSION}_i386.snap --release stable
snapcraft push statping_${VERSION}_armhf.snap --release stable
snapcraft push statping_${VERSION}_arm.snap --release stable
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-bin build-win build-all
postman: clean compile
API_SECRET=demosecret123 statping --port=8080 > /dev/null &
sleep 3
newman run -e dev/postman_environment.json dev/postman.json
killall statping
.PHONY: all build build-all build-alpine test-all test test-api docker frontend up down print_details lite sentry-release snapcraft build-linux build-mac build-win build-all postman
.SILENT: travis_s3_creds

View File

@ -9,13 +9,15 @@
# Statping - Status Page & Monitoring Server
An easy to use Status Page for your websites and applications. Statping will automatically fetch the application and render a beautiful status page with tons of features for you to build an even better status page. This Status Page generator allows you to use MySQL, Postgres, or SQLite on multiple operating systems.
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/statping/statping) [![Slack](https://slack.statping.com/badge.svg)](https://slack.statping.com) [![](https://images.microbadger.com/badges/image/statping/statping.svg)](https://microbadger.com/images/statping/statping) [![Docker Pulls](https://img.shields.io/docker/pulls/statping/statping.svg)](https://hub.docker.com/r/statping/statping/builds/)
[![Latest](https://github.com/statping/statping/workflows/Master%20Release/badge.svg)](https://github.com/statping/statping/actions) [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/statping/statping) [![Slack](https://slack.statping.com/badge.svg)](https://slack.statping.com) [![](https://images.microbadger.com/badges/image/statping/statping.svg)](https://microbadger.com/images/statping/statping) [![Docker Pulls](https://img.shields.io/docker/pulls/statping/statping.svg)](https://hub.docker.com/r/statping/statping/builds/)
<br><br>
<img align="left" width="320" height="235" src="https://img.cjx.io/statupsiterun.gif">
<h2>A Future-Proof Status Page</h2>
Statping strives to remain future-proof and remain intact if a failure is created. Your Statping service should not be running on the same instance you're trying to monitor. If your server crashes your Status Page should still remaining online to notify your users of downtime.
<br><br><br><br><br>
<br><a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/statping/statping/master/dev/pwd-stack.yml"><img height=25 src="https://assets.statping.com/docker-pwd.png"></a> (dashboard login is `admin`, password `admin`)
<br><br><br>
<h2>No Requirements</h2>
Statping is built in Go Language so all you need is the precompile binary based on your operating system. You won't need to install anything extra once you have the Statping binary installed. You can even run Statping on a Raspberry Pi.
@ -38,7 +40,7 @@ The Status binary for all other OS's is ~17Mb at most.
<img align="left" width="320" height="235" src="https://img.cjx.io/statping_iphone_bk.png">
<h2>Mobile App is Gorgeous</h2>
The Statping app is available on the App Store and Google Play for free. The app will allow you to view services, receive notifications when a service is offline, update groups, users, services, messages, and more! Start your own Statping server and then connect it to the app by scanning the QR code in settings.
The Statping app is available on the App Store and Google Play for free. The app will allow you to view services, receive notifications when a service is offline, update groups, users, services, messages, and more! Start your own Statping server and then connect it to the app by scanning the QR code in settings.
<p align="center">
<a href="https://play.google.com/store/apps/details?id=com.statping"><img src="https://img.cjx.io/google-play.svg"></a>

View File

@ -1,30 +0,0 @@
package main
import (
"fmt"
"github.com/statping/statping/source"
"github.com/statping/statping/utils"
"net"
)
// UsingAssets will return true if /assets folder is present
func UsingAssets() bool {
return source.UsingAssets(utils.Directory)
}
// GetLocalIP returns the non loopback local IP of the host
func GetLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "http://localhost"
}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return fmt.Sprintf("http://%v", ipnet.IP.String())
}
}
}
return "http://localhost"
}

View File

@ -4,13 +4,15 @@ import (
"bufio"
"encoding/json"
"fmt"
"github.com/joho/godotenv"
"github.com/pkg/errors"
"github.com/statping/statping/handlers"
"github.com/statping/statping/source"
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/configs"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/groups"
"github.com/statping/statping/types/messages"
"github.com/statping/statping/types/services"
"github.com/statping/statping/types/users"
"github.com/statping/statping/utils"
"io/ioutil"
"os"
@ -18,209 +20,213 @@ import (
"time"
)
// catchCLI will run functions based on the commands sent to Statping
func catchCLI(args []string) error {
func assetsCli() error {
dir := utils.Directory
runLogs := utils.InitLogs
runAssets := source.Assets
if err := utils.InitLogs(); err != nil {
return err
}
if err := source.Assets(); err != nil {
return err
}
if err := source.CreateAllAssets(dir); err != nil {
return err
}
return nil
}
switch args[0] {
case "version":
if COMMIT != "" {
fmt.Printf("%s (%s)\n", VERSION, COMMIT)
} else {
fmt.Printf("%s\n", VERSION)
}
return errors.New("end")
case "assets":
var err error
if err = runLogs(); err != nil {
return err
}
if err = runAssets(); err != nil {
return err
}
if err = source.CreateAllAssets(dir); err != nil {
return err
}
return errors.New("end")
case "sass":
if err := runLogs(); err != nil {
return err
}
if err := runAssets(); err != nil {
return err
}
if err := source.CompileSASS(source.DefaultScss...); err != nil {
return err
}
return errors.New("end")
case "update":
updateDisplay()
return errors.New("end")
case "static":
//var err error
//if err = runLogs(); err != nil {
// return err
//}
//if err = runAssets(); err != nil {
// return err
//}
//fmt.Printf("Statping v%v Exporting Static 'index.html' page...\n", VERSION)
//if _, err = core.LoadConfigFile(dir); err != nil {
// log.Errorln("config.yml file not found")
// return err
//}
//indexSource := ExportIndexHTML()
////core.CloseDB()
//if err = utils.SaveFile(dir+"/index.html", indexSource); err != nil {
// log.Errorln(err)
// return err
//}
//log.Infoln("Exported Statping index page: 'index.html'")
case "help":
HelpEcho()
return errors.New("end")
case "export":
var err error
var data []byte
if err = runLogs(); err != nil {
return err
}
if err = runAssets(); err != nil {
return err
}
config, err := configs.LoadConfigs()
if err != nil {
return err
}
if err = configs.ConnectConfigs(config); err != nil {
return err
}
if _, err := services.SelectAllServices(false); err != nil {
return err
}
if data, err = handlers.ExportSettings(); err != nil {
return fmt.Errorf("could not export settings: %v", err.Error())
}
filename := fmt.Sprintf("%s/statping-%s.json", dir, time.Now().Format("01-02-2006-1504"))
if err = utils.SaveFile(filename, data); err != nil {
return fmt.Errorf("could not write file statping-export.json: %v", err.Error())
}
log.Infoln("Statping export file saved to ", filename)
return errors.New("end")
case "import":
var err error
var data []byte
if len(args) != 2 {
return fmt.Errorf("did not include a JSON file to import\nstatping import filename.json")
}
filename := args[1]
if data, err = ioutil.ReadFile(filename); err != nil {
return err
}
var exportData handlers.ExportData
if err = json.Unmarshal(data, &exportData); err != nil {
return err
}
log.Printf("=== %s ===\n", exportData.Core.Name)
log.Printf("Services: %d\n", len(exportData.Services))
log.Printf("Checkins: %d\n", len(exportData.Checkins))
log.Printf("Groups: %d\n", len(exportData.Groups))
log.Printf("Messages: %d\n", len(exportData.Messages))
log.Printf("Users: %d\n", len(exportData.Users))
func exportCli(args []string) error {
filename := fmt.Sprintf("%s/statping-%s.json", utils.Directory, time.Now().Format("01-02-2006-1504"))
if len(args) == 1 {
filename = fmt.Sprintf("%s/%s", utils.Directory, args)
}
var data []byte
if err := utils.InitLogs(); err != nil {
return err
}
if err := source.Assets(); err != nil {
return err
}
config, err := configs.LoadConfigs()
if err != nil {
return err
}
if err = configs.ConnectConfigs(config, false); err != nil {
return err
}
if _, err := services.SelectAllServices(false); err != nil {
return err
}
if data, err = ExportSettings(); err != nil {
return fmt.Errorf("could not export settings: %v", err.Error())
}
if err = utils.SaveFile(filename, data); err != nil {
return fmt.Errorf("could not write file statping-export.json: %v", err.Error())
}
log.Infoln("Statping export file saved to ", filename)
return nil
}
config, err := configs.LoadConfigs()
if err != nil {
return err
}
if err = configs.ConnectConfigs(config); err != nil {
return err
}
if data, err = handlers.ExportSettings(); err != nil {
return fmt.Errorf("could not export settings: %v", err.Error())
}
func sassCli() error {
if err := utils.InitLogs(); err != nil {
return err
}
if err := source.Assets(); err != nil {
return err
}
if err := source.CompileSASS(source.DefaultScss...); err != nil {
return err
}
return nil
}
if ask("Import Core settings?") {
c := exportData.Core
if err := c.Update(); err != nil {
func resetCli() error {
d := utils.Directory
fmt.Println("Statping directory: ", d)
assets := d + "/assets"
if utils.FolderExists(assets) {
fmt.Printf("Deleting %s folder.\n", assets)
if err := utils.DeleteDirectory(assets); err != nil {
return err
}
} else {
fmt.Printf("Assets folder does not exist %s\n", assets)
}
logDir := d + "/logs"
if utils.FolderExists(logDir) {
fmt.Printf("Deleting %s directory.\n", logDir)
if err := utils.DeleteDirectory(logDir); err != nil {
return err
}
} else {
fmt.Printf("Logs folder does not exist %s\n", logDir)
}
c := d + "/config.yml"
if utils.FileExists(c) {
fmt.Printf("Deleting %s file.\n", c)
if err := utils.DeleteFile(c); err != nil {
return err
}
} else {
fmt.Printf("Config file does not exist %s\n", c)
}
dbFile := d + "/statping.db"
if utils.FileExists(dbFile) {
fmt.Printf("Backuping up %s file.\n", dbFile)
if err := utils.RenameDirectory(dbFile, d+"/statping.db.backup"); err != nil {
return err
}
} else {
fmt.Printf("Statping SQL Database file does not exist %s\n", dbFile)
}
fmt.Println("Statping has been reset")
return nil
}
func envCli() error {
fmt.Println("Statping Configuration")
for k, v := range utils.Params.AllSettings() {
fmt.Printf("%s=%v\n", strings.ToUpper(k), v)
}
return nil
}
func onceCli() error {
if err := utils.InitLogs(); err != nil {
return err
}
if err := source.Assets(); err != nil {
return err
}
log.Infoln("Running 1 time and saving to database...")
if err := runOnce(); err != nil {
return err
}
//core.CloseDB()
fmt.Println("Check is complete.")
return nil
}
func importCli(args []string) error {
var err error
var data []byte
filename := args[1]
if data, err = ioutil.ReadFile(filename); err != nil {
return err
}
var exportData ExportData
if err = json.Unmarshal(data, &exportData); err != nil {
return err
}
log.Printf("=== %s ===\n", exportData.Core.Name)
log.Printf("Services: %d\n", len(exportData.Services))
log.Printf("Checkins: %d\n", len(exportData.Checkins))
log.Printf("Groups: %d\n", len(exportData.Groups))
log.Printf("Messages: %d\n", len(exportData.Messages))
log.Printf("Users: %d\n", len(exportData.Users))
config, err := configs.LoadConfigs()
if err != nil {
return err
}
if err = configs.ConnectConfigs(config, false); err != nil {
return err
}
if data, err = ExportSettings(); err != nil {
return fmt.Errorf("could not export settings: %v", err.Error())
}
if ask("Import Core settings?") {
c := exportData.Core
if err := c.Update(); err != nil {
return err
}
}
for _, s := range exportData.Groups {
if ask(fmt.Sprintf("Import Group '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
for _, s := range exportData.Groups {
if ask(fmt.Sprintf("Import Group '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Services {
if ask(fmt.Sprintf("Import Service '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Checkins {
if ask(fmt.Sprintf("Import Checkin '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Messages {
if ask(fmt.Sprintf("Import Message '%s'?", s.Title)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Users {
if ask(fmt.Sprintf("Import User '%s'?", s.Username)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
log.Infof("Import complete")
return errors.New("end")
case "run":
if err := runLogs(); err != nil {
return err
}
if err := runAssets(); err != nil {
return err
}
log.Infoln("Running 1 time and saving to database...")
runOnce()
//core.CloseDB()
fmt.Println("Check is complete.")
return errors.New("end")
case "env":
fmt.Println("Statping Environment Variable")
if err := runLogs(); err != nil {
return err
}
if err := runAssets(); err != nil {
return err
}
envs, err := godotenv.Read(".env")
if err != nil {
log.Errorln("No .env file found in current directory.")
return err
}
for k, e := range envs {
fmt.Printf("%v=%v\n", k, e)
}
default:
return nil
}
return errors.New("end")
for _, s := range exportData.Services {
if ask(fmt.Sprintf("Import Service '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Checkins {
if ask(fmt.Sprintf("Import Checkin '%s'?", s.Name)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Messages {
if ask(fmt.Sprintf("Import Message '%s'?", s.Title)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
for _, s := range exportData.Users {
if ask(fmt.Sprintf("Import User '%s'?", s.Username)) {
s.Id = 0
if err := s.Create(); err != nil {
return err
}
}
}
log.Infof("Import complete")
return nil
}
func ask(format string) bool {
@ -231,21 +237,6 @@ func ask(format string) bool {
return strings.ToLower(text) == "y"
}
// ExportIndexHTML returns the HTML of the index page as a string
//func ExportIndexHTML() []byte {
// source.Assets()
// core.CoreApp.Connect(core.CoreApp., utils.Directory)
// core.SelectAllServices(false)
// core.CoreApp.UseCdn = types.NewNullBool(true)
// for _, srv := range core.Services() {
// core.CheckService(srv, true)
// }
// w := httptest.NewRecorder()
// r := httptest.NewRequest("GET", "/", nil)
// handlers.ExecuteResponse(w, r, "index.gohtml", nil, nil)
// return w.Body.Bytes()
//}
func updateDisplay() error {
gitCurrent, err := checkGithubUpdates()
if err != nil {
@ -271,7 +262,7 @@ func runOnce() error {
if err != nil {
return errors.Wrap(err, "config.yml file not found")
}
err = configs.ConnectConfigs(config)
err = configs.ConnectConfigs(config, false)
if err != nil {
return errors.Wrap(err, "issue connecting to database")
}
@ -292,62 +283,6 @@ func runOnce() error {
return nil
}
// HelpEcho prints out available commands and flags for Statping
func HelpEcho() {
fmt.Printf("Statping v%v - Statping.com\n", VERSION)
fmt.Printf("A simple Application Status Monitor that is opensource and lightweight.\n")
fmt.Printf("Commands:\n")
fmt.Println(" statping - Main command to run Statping server")
fmt.Println(" statping version - Returns the current version of Statping")
fmt.Println(" statping run - Check all services 1 time and then quit")
fmt.Println(" statping assets - Dump all assets used locally to be edited.")
//fmt.Println(" statping static - Creates a static HTML file of the index page")
fmt.Println(" statping sass - Compile .scss files into the css directory")
fmt.Println(" statping env - Show all environment variables being used for Statping")
fmt.Println(" statping update - Attempts to update to the latest version")
fmt.Println(" statping export - Exports your Statping settings to a 'statping-export.json' file.")
fmt.Println(" statping import <file> - Imports settings from a previously saved JSON file.")
fmt.Println(" statping help - Shows the user basic information about Statping")
fmt.Printf("Flags:\n")
fmt.Println(" -ip 127.0.0.1 - Run HTTP server on specific IP address (default: localhost)")
fmt.Println(" -port 8080 - Run HTTP server on Port (default: 8080)")
fmt.Println(" -verbose 1 - Verbose mode levels 1 - 4 (default: 1)")
fmt.Println(" -env path/debug.env - Optional .env file to set as environment variables while running server")
fmt.Printf("Environment Variables:\n")
fmt.Println(" PORT - Set the outgoing port for the HTTP server (or use -port)")
fmt.Println(" IP - Bind a specific IP address to the HTTP server (or use -ip)")
fmt.Println(" VERBOSE - Display more logs in verbose mode. (1 - 4)")
fmt.Println(" STATPING_DIR - Set a absolute path for the root path of Statping server (logs, assets, SQL db)")
fmt.Println(" DB_CONN - Automatic Database connection (sqlite, postgres, mysql)")
fmt.Println(" DB_HOST - Database hostname or IP address")
fmt.Println(" DB_USER - Database username")
fmt.Println(" DB_PASS - Database password")
fmt.Println(" DB_PORT - Database port (5432, 3306, ...)")
fmt.Println(" DB_DATABASE - Database connection's database name")
fmt.Println(" POSTGRES_SSLMODE - Enable Postgres SSL Mode 'ssl_mode=VALUE' (enable/disable/verify-full/verify-ca)")
fmt.Println(" DISABLE_LOGS - Disable viewing and writing to the log file (default is false)")
fmt.Println(" GO_ENV - Run Statping in testmode, will bypass HTTP authentication (if set as 'test')")
fmt.Println(" NAME - Set a name for the Statping status page")
fmt.Println(" DESCRIPTION - Set a description for the Statping status page")
fmt.Println(" DOMAIN - Set a URL for the Statping status page")
fmt.Println(" ADMIN_USER - Username for administrator account (default: admin)")
fmt.Println(" ADMIN_PASS - Password for administrator account (default: admin)")
fmt.Println(" SASS - Set the absolute path to the sass binary location")
fmt.Println(" USE_ASSETS - Automatically use assets from 'assets folder' (true/false)")
fmt.Println(" HTTP_PROXY - Use a HTTP Proxy for HTTP Requests")
fmt.Println(" AUTH_USERNAME - HTTP Basic Authentication username")
fmt.Println(" AUTH_PASSWORD - HTTP Basic Authentication password")
fmt.Println(" BASE_PATH - Set the base URL prefix (set to 'monitor' if URL is domain.com/monitor)")
fmt.Println(" PREFIX - A Prefix for each value in Prometheus /metric exporter")
fmt.Println(" API_KEY - Set a custom API Key for Statping")
fmt.Println(" API_SECRET - Set a custom API Secret for API Authentication")
fmt.Println(" MAX_OPEN_CONN - Set Maximum Open Connections for database server (default: 5)")
fmt.Println(" MAX_IDLE_CONN - Set Maximum Idle Connections for database server")
fmt.Println(" MAX_LIFE_CONN - Set Maximum Life Connections for database server")
fmt.Println(" * You can insert environment variables into a '.env' file in root directory.")
fmt.Println("Give Statping a Star at https://github.com/statping/statping")
}
func checkGithubUpdates() (githubResponse, error) {
url := "https://api.github.com/repos/statping/statping/releases/latest"
contents, _, err := utils.HttpRequest(url, "GET", nil, nil, nil, time.Duration(2*time.Second), true)
@ -437,3 +372,56 @@ type gitUploader struct {
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
}
// ExportChartsJs renders the charts for the index page
type ExportData struct {
Core *core.Core `json:"core"`
Services []services.Service `json:"services"`
Messages []*messages.Message `json:"messages"`
Checkins []*checkins.Checkin `json:"checkins"`
Users []*users.User `json:"users"`
Groups []*groups.Group `json:"groups"`
Notifiers []core.AllNotifiers `json:"notifiers"`
}
// ExportSettings will export a JSON file containing all of the settings below:
// - Core
// - Notifiers
// - Checkins
// - Users
// - Services
// - Groups
// - Messages
func ExportSettings() ([]byte, error) {
c, err := core.Select()
if err != nil {
return nil, err
}
data := ExportData{
Core: c,
//Notifiers: notifications.All(),
Checkins: checkins.All(),
Users: users.All(),
Services: services.AllInOrder(),
Groups: groups.All(),
Messages: messages.All(),
}
export, err := json.Marshal(data)
return export, err
}
// ExportIndexHTML returns the HTML of the index page as a string
//func ExportIndexHTML() []byte {
// source.Assets()
// core.CoreApp.Connect(core.CoreApp., utils.Directory)
// core.SelectAllServices(false)
// core.CoreApp.UseCdn = types.NewNullBool(true)
// for _, srv := range core.Services() {
// core.CheckService(srv, true)
// }
// w := httptest.NewRecorder()
// r := httptest.NewRequest("GET", "/", nil)
// handlers.ExecuteResponse(w, r, "index.gohtml", nil, nil)
// return w.Body.Bytes()
//}

View File

@ -1,14 +1,12 @@
package main
import (
"github.com/rendon/testcli"
"bytes"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"os/exec"
"io/ioutil"
"testing"
"time"
)
var (
@ -16,169 +14,95 @@ var (
)
func init() {
dir = utils.Directory
//core.SampleHits = 480
utils.InitCLI()
}
func TestStartServerCommand(t *testing.T) {
t.SkipNow()
os.Setenv("DB_CONN", "sqlite")
cmd := helperCommand(nil, "")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(60*time.Second), got)
os.Unsetenv("DB_CONN")
gg, _ := <-got
assert.Contains(t, gg, "DB_CONN environment variable was found")
assert.Contains(t, gg, "Core database does not exist, creating now!")
assert.Contains(t, gg, "Starting monitoring process for 5 Services")
}
func TestStatpingDirectory(t *testing.T) {
dir := utils.Directory
require.NotContains(t, dir, "/cmd")
require.NotEmpty(t, dir)
func TestVersionCommand(t *testing.T) {
c := testcli.Command("statping", "version")
c.Run()
assert.True(t, c.StdoutContains(VERSION))
}
func TestHelpCommand(t *testing.T) {
c := testcli.Command("statping", "help")
c.Run()
t.Log(c.Stdout())
assert.True(t, c.StdoutContains("statping help - Shows the user basic information about Statping"))
}
func TestStaticCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "static")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(10*time.Second), got)
gg, _ := <-got
t.Log(gg)
assert.Contains(t, gg, "Exporting Static 'index.html' page...")
assert.Contains(t, gg, "Exported Statping index page: 'index.html'")
assert.FileExists(t, dir+"/index.html")
}
func TestExportCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "export")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(10*time.Second), got)
gg, _ := <-got
t.Log(gg)
assert.FileExists(t, dir+"/statping-export.json")
}
func TestUpdateCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "version")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(15*time.Second), got)
gg, _ := <-got
t.Log(gg)
assert.Contains(t, gg, VERSION)
}
func TestAssetsCommand(t *testing.T) {
t.SkipNow()
c := testcli.Command("statping", "assets")
c.Run()
t.Log(c.Stdout())
t.Log("Directory for Assets: ", dir)
time.Sleep(1 * time.Second)
err := utils.DeleteDirectory(dir + "/assets")
require.Nil(t, err)
assert.FileExists(t, dir+"/assets/robots.txt")
assert.FileExists(t, dir+"/assets/scss/base.scss")
assert.FileExists(t, dir+"/assets/scss/main.scss")
assert.FileExists(t, dir+"/assets/scss/variables.scss")
assert.FileExists(t, dir+"/assets/css/main.css")
assert.FileExists(t, dir+"/assets/css/vendor.css")
assert.FileExists(t, dir+"/assets/css/style.css")
err = utils.DeleteDirectory(dir + "/assets")
require.Nil(t, err)
}
func TestRunCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "run")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(15*time.Second), got)
gg, _ := <-got
t.Log(gg)
assert.Contains(t, gg, "Running 1 time and saving to database...")
assert.Contains(t, gg, "Check is complete.")
}
func TestEnvironmentVarsCommand(t *testing.T) {
c := testcli.Command("statping", "env")
c.Run()
assert.True(t, c.StdoutContains("Statping Environment Variable"))
}
func TestVersionCLI(t *testing.T) {
run := catchCLI([]string{"version"})
assert.EqualError(t, run, "end")
}
func TestAssetsCLI(t *testing.T) {
catchCLI([]string{"assets"})
//assert.EqualError(t, run, "end")
assert.FileExists(t, dir+"/assets/css/main.css")
assert.FileExists(t, dir+"/assets/css/style.css")
assert.FileExists(t, dir+"/assets/css/vendor.css")
assert.FileExists(t, dir+"/assets/scss/base.scss")
assert.FileExists(t, dir+"/assets/scss/mobile.scss")
assert.FileExists(t, dir+"/assets/scss/variables.scss")
}
func TestSassCLI(t *testing.T) {
t.SkipNow()
catchCLI([]string{"sass"})
assert.FileExists(t, dir+"/assets/css/main.css")
assert.FileExists(t, dir+"/assets/css/style.css")
assert.FileExists(t, dir+"/assets/css/vendor.css")
}
func TestUpdateCLI(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "update")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(15*time.Second), got)
gg, _ := <-got
t.Log(gg)
assert.Contains(t, gg, "version")
}
func TestHelpCLI(t *testing.T) {
run := catchCLI([]string{"help"})
assert.EqualError(t, run, "end")
}
func TestRunOnceCLI(t *testing.T) {
t.SkipNow()
run := catchCLI([]string{"run"})
assert.EqualError(t, run, "end")
dir = utils.Params.GetString("STATPING_DIR")
require.NotContains(t, dir, "/cmd")
require.NotEmpty(t, dir)
}
func TestEnvCLI(t *testing.T) {
run := catchCLI([]string{"env"})
assert.Error(t, run)
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"env"})
err := cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
require.Nil(t, err)
assert.Contains(t, string(out), VERSION)
assert.Contains(t, utils.Directory, string(out))
assert.Contains(t, "SAMPLE_DATA=true", string(out))
}
func commandAndSleep(cmd *exec.Cmd, duration time.Duration, out chan<- string) {
go func(out chan<- string) {
runCommand(cmd, out)
}(out)
time.Sleep(duration)
cmd.Process.Kill()
func TestVersionCLI(t *testing.T) {
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"version"})
err := cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
require.Nil(t, err)
assert.Contains(t, VERSION, string(out))
}
func helperCommand(envs []string, s ...string) *exec.Cmd {
cmd := exec.Command("statping", s...)
return cmd
func TestAssetsCLI(t *testing.T) {
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"assets"})
err := cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
assert.Nil(t, err)
assert.Contains(t, string(out), VERSION)
assert.FileExists(t, utils.Directory+"/assets/css/main.css")
assert.FileExists(t, utils.Directory+"/assets/css/style.css")
assert.FileExists(t, utils.Directory+"/assets/css/vendor.css")
assert.FileExists(t, utils.Directory+"/assets/scss/base.scss")
assert.FileExists(t, utils.Directory+"/assets/scss/mobile.scss")
assert.FileExists(t, utils.Directory+"/assets/scss/variables.scss")
}
func runCommand(c *exec.Cmd, out chan<- string) {
bout, _ := c.CombinedOutput()
out <- string(bout)
func TestHelpCLI(t *testing.T) {
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"help"})
err := cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
require.Nil(t, err)
assert.Contains(t, string(out), VERSION)
}
func TestResetCLI(t *testing.T) {
err := utils.SaveFile(utils.Directory+"/statping.db", []byte("test data"))
require.Nil(t, err)
cmd := rootCmd
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"reset"})
err = cmd.Execute()
require.Nil(t, err)
out, err := ioutil.ReadAll(b)
require.Nil(t, err)
assert.Contains(t, string(out), VERSION)
assert.NoDirExists(t, utils.Directory+"/assets")
assert.NoDirExists(t, utils.Directory+"/logs")
assert.NoFileExists(t, utils.Directory+"/config.yml")
assert.NoFileExists(t, utils.Directory+"/statping.db")
assert.FileExists(t, utils.Directory+"/statping.db.backup")
err = utils.DeleteFile(utils.Directory + "/statping.db.backup")
require.Nil(t, err)
}

95
cmd/commands.go Normal file
View File

@ -0,0 +1,95 @@
package main
import (
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "statping",
Short: "A simple Application Status Monitor that is opensource and lightweight.",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Statping",
Run: func(cmd *cobra.Command, args []string) {
if COMMIT != "" {
fmt.Printf("%s (%s)\n", VERSION, COMMIT)
} else {
fmt.Printf("%s\n", VERSION)
}
},
}
var assetsCmd = &cobra.Command{
Use: "assets",
Short: "Dump all assets used locally to be edited",
RunE: func(cmd *cobra.Command, args []string) error {
return assetsCli()
},
}
var exportCmd = &cobra.Command{
Use: "export",
Short: "Exports your Statping settings to a 'statping-export.json' file.",
RunE: func(cmd *cobra.Command, args []string) error {
return exportCli(args)
},
}
var sassCmd = &cobra.Command{
Use: "sass",
Short: "Compile .scss files into the css directory",
RunE: func(cmd *cobra.Command, args []string) error {
return sassCli()
},
}
var envCmd = &cobra.Command{
Use: "env",
Short: "Return the configs that will be ran",
RunE: func(cmd *cobra.Command, args []string) error {
return envCli()
},
}
var resetCmd = &cobra.Command{
Use: "reset",
Short: "Start a fresh copy of Statping",
RunE: func(cmd *cobra.Command, args []string) error {
return resetCli()
},
}
var onceCmd = &cobra.Command{
Use: "once",
Short: "Check all services 1 time and then quit",
RunE: func(cmd *cobra.Command, args []string) error {
return onceCli()
},
}
var importCmd = &cobra.Command{
Use: "import [.json file]",
Short: "Imports settings from a previously saved JSON file.",
RunE: func(cmd *cobra.Command, args []string) error {
return importCli(args)
},
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires input file (.json)")
}
return nil
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
exit(err)
}
}

17
cmd/flags.go Normal file
View File

@ -0,0 +1,17 @@
package main
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
func parseFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&ipAddress, "ip", "s", "0.0.0.0", "server port")
viper.BindPFlag("ip", cmd.PersistentFlags().Lookup("ip"))
cmd.PersistentFlags().IntVarP(&port, "port", "p", 8080, "server port")
viper.BindPFlag("port", cmd.PersistentFlags().Lookup("port"))
cmd.PersistentFlags().IntVarP(&verboseMode, "verbose", "v", 2, "server port")
viper.BindPFlag("verbose", cmd.PersistentFlags().Lookup("verbose"))
}

View File

@ -1,7 +1,6 @@
package main
import (
"flag"
"fmt"
"github.com/pkg/errors"
"github.com/statping/statping/database"
@ -31,26 +30,18 @@ var (
confgs *configs.DbConfig
)
// parseFlags will parse the application flags
// -ip = 0.0.0.0 IP address for outgoing HTTP server
// -port = 8080 Port number for outgoing HTTP server
// environment variables WILL overwrite flags
func parseFlags() {
envPort := utils.Getenv("PORT", 8080).(int)
envIpAddress := utils.Getenv("IP", "0.0.0.0").(string)
envVerbose := utils.Getenv("VERBOSE", 2).(int)
//envGrpcPort := utils.Getenv("GRPC_PORT", 0).(int)
flag.StringVar(&ipAddress, "ip", envIpAddress, "IP address to run the Statping HTTP server")
flag.StringVar(&envFile, "env", "", "IP address to run the Statping HTTP server")
flag.IntVar(&port, "port", envPort, "Port to run the HTTP server")
//flag.IntVar(&grpcPort, "grpc", envGrpcPort, "Port to run the gRPC server")
flag.IntVar(&verboseMode, "verbose", envVerbose, "Run in verbose mode to see detailed logs (1 - 4)")
flag.Parse()
}
func init() {
core.New(VERSION)
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(assetsCmd)
rootCmd.AddCommand(exportCmd)
rootCmd.AddCommand(importCmd)
rootCmd.AddCommand(sassCmd)
rootCmd.AddCommand(onceCmd)
rootCmd.AddCommand(envCmd)
rootCmd.AddCommand(resetCmd)
utils.InitCLI()
parseFlags(rootCmd)
}
// exit will return an error and return an exit code 1 due to this error
@ -69,11 +60,14 @@ func Close() {
// main will run the Statping application
func main() {
Execute()
}
// main will run the Statping application
func start() {
var err error
go sigterm()
parseFlags()
if err := source.Assets(); err != nil {
exit(err)
}
@ -84,32 +78,21 @@ func main() {
log.Errorf("Statping Log Error: %v\n", err)
}
args := flag.Args()
if len(args) >= 1 {
err := catchCLI(args)
if err != nil {
if err.Error() == "end" {
os.Exit(0)
return
}
exit(err)
}
}
log.Info(fmt.Sprintf("Starting Statping v%s", VERSION))
if err := updateDisplay(); err != nil {
log.Warnln(err)
}
//if err := updateDisplay(); err != nil {
// log.Warnln(err)
//}
confgs, err = configs.LoadConfigs()
if err != nil {
log.Infoln("Starting in Setup Mode")
if err := SetupMode(); err != nil {
exit(err)
}
}
if err = configs.ConnectConfigs(confgs); err != nil {
if err = configs.ConnectConfigs(confgs, true); err != nil {
exit(err)
}
@ -135,8 +118,10 @@ func main() {
exit(errors.Wrap(err, "error creating default admin user"))
}
if err := configs.TriggerSamples(); err != nil {
exit(errors.Wrap(err, "error creating database"))
if utils.Params.GetBool("SAMPLE_DATA") {
if err := configs.TriggerSamples(); err != nil {
exit(errors.Wrap(err, "error creating database"))
}
}
}
@ -149,12 +134,6 @@ func main() {
exit(err)
}
//log.Infoln("Migrating Notifiers...")
//if err := notifier.Migrate(); err != nil {
// exit(errors.Wrap(err, "error migrating notifiers"))
//}
//log.Infoln("Notifiers Migrated")
if err := mainProcess(); err != nil {
exit(err)
}

View File

@ -92,6 +92,7 @@ type Database interface {
// extra
Error() error
Status() int
RowsAffected() int64
Since(time.Time) Database
@ -156,7 +157,10 @@ type Db struct {
// Openw is a drop-in replacement for Open()
func Openw(dialect string, args ...interface{}) (db Database, err error) {
gorm.NowFunc = func() time.Time {
return time.Now().UTC()
return utils.Now()
}
if dialect == "sqlite" {
dialect = "sqlite3"
}
gormdb, err := gorm.Open(dialect, args...)
if err != nil {
@ -167,22 +171,36 @@ func Openw(dialect string, args ...interface{}) (db Database, err error) {
}
func OpenTester() (Database, error) {
testDB := utils.Getenv("TEST_DB", "sqlite3").(string)
var dbParamsstring string
testDB := utils.Params.GetString("DB_CONN")
var dbString string
switch testDB {
case "mysql":
dbParamsstring = fmt.Sprintf("root:password123@tcp(localhost:3306)/statping?charset=utf8&parseTime=True&loc=UTC&time_zone=%%27UTC%%27")
dbString = fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=utf8&parseTime=True&loc=UTC&time_zone=%%27UTC%%27",
utils.Params.GetString("DB_HOST"),
utils.Params.GetString("DB_PASS"),
utils.Params.GetString("DB_HOST"),
utils.Params.GetInt("DB_PORT"),
utils.Params.GetString("DB_DATABASE"),
)
case "postgres":
dbParamsstring = fmt.Sprintf("host=localhost port=5432 user=root dbname=statping password=password123 timezone=UTC")
dbString = fmt.Sprintf("host=%s port=%v user=%s dbname=%s password=%s sslmode=disable timezone=UTC",
utils.Params.GetString("DB_HOST"),
utils.Params.GetInt("DB_PORT"),
utils.Params.GetString("DB_USER"),
utils.Params.GetString("DB_DATABASE"),
utils.Params.GetString("DB_PASS"))
default:
dbParamsstring = fmt.Sprintf("file:%s?mode=memory&cache=shared", utils.RandomString(12))
dbString = fmt.Sprintf("file:%s?mode=memory&cache=shared", utils.RandomString(12))
}
fmt.Println(testDB, dbParamsstring)
newDb, err := Openw(testDB, dbParamsstring)
newDb, err := Openw(testDB, dbString)
if err != nil {
return nil, err
}
newDb.DB().SetMaxOpenConns(1)
if testDB != "sqlite3" {
newDb.DB().SetMaxOpenConns(25)
}
return newDb, err
}
@ -490,6 +508,34 @@ func (it *Db) Error() error {
return it.Database.Error
}
func (it *Db) Status() int {
switch it.Database.Error {
case gorm.ErrRecordNotFound:
return 404
case gorm.ErrCantStartTransaction:
return 422
case gorm.ErrInvalidSQL:
return 500
case gorm.ErrUnaddressable:
return 500
default:
return 500
}
}
func (it *Db) Loggable() bool {
switch it.Database.Error {
case gorm.ErrCantStartTransaction:
return true
case gorm.ErrInvalidSQL:
return true
case gorm.ErrUnaddressable:
return true
default:
return false
}
}
func (it *Db) Since(ago time.Time) Database {
return it.Where("created_at > ?", it.FormatTime(ago))
}

View File

@ -74,7 +74,7 @@ func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
dbQuery := g.db.MultipleSelects(
g.db.SelectByTime(g.Group),
by.String(),
).Group("timeframe")
).Group("timeframe").Order("timeframe", true)
g.db = dbQuery

View File

@ -17,8 +17,8 @@ var (
// Maintenance will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
func Maintenance() {
dur := utils.GetenvAs("REMOVE_AFTER", "2160h").Duration()
interval := utils.GetenvAs("CLEANUP_INTERVAL", "1h").Duration()
dur := utils.Params.GetDuration("REMOVE_AFTER")
interval := utils.Params.GetDuration("CLEANUP_INTERVAL")
log.Infof("Database Cleanup runs every %s and will remove records older than %s", interval.String(), dur.String())
ticker := interval

View File

@ -36,7 +36,7 @@ func (it *Db) SelectByTime(increment time.Duration) string {
case "mysql":
return fmt.Sprintf("FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(created_at) / %d) * %d) AS timeframe", seconds, seconds)
case "postgres":
return fmt.Sprintf("date_trunc('%s', created_at) AS timeframe", increment)
return fmt.Sprintf("date_trunc('minute', created_at) - (CAST(EXTRACT(MINUTE FROM created_at) AS integer) %% %d) * interval '1 minute' AS timeframe", seconds)
default:
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %d) * %d, 'unixepoch') as timeframe", seconds, seconds)
}

View File

@ -5,14 +5,12 @@ services:
postgres:
container_name: postgres
image: postgres
volumes:
- ../docker/databases/postgres:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password123
POSTGRES_DB: statping
POSTGRES_USER: root
ports:
- 5432:5432
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U root"]
interval: 15s
@ -22,8 +20,6 @@ services:
mysql:
container_name: mysql
image: mysql:5.7
volumes:
- ../docker/databases/mysql:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: password123
@ -31,7 +27,7 @@ services:
MYSQL_USER: root
MYSQL_PASSWORD: password123
ports:
- 3306:3306
- "127.0.0.1:3306:3306"
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
timeout: 20s
@ -46,7 +42,7 @@ services:
mysql:
condition: service_healthy
ports:
- 5050:80
- "127.0.0.1:5050:80"
links:
- mysql:db
environment:
@ -62,7 +58,7 @@ services:
restart: on-failure
command: sqlite_web -H 0.0.0.0 -r -x /data/statping.db
ports:
- 6050:8080
- "127.0.0.1:6050:8080"
volumes:
- ../docker/statping/sqlite/statping.db:/data/statping.db:ro
environment:
@ -75,11 +71,8 @@ services:
environment:
DEFAULT_USER: admin@admin.com
DEFAULT_PASSWORD: admin
depends_on:
postgres:
condition: service_healthy
ports:
- 7000:5050
- "127.0.0.1:7000:5050"
links:
- postgres:postgres
@ -91,7 +84,7 @@ services:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- ../docker/databases/prometheus:/prometheus
ports:
- 7050:9090
- "127.0.0.1:7050:9090"
healthcheck:
test: "/bin/wget -q -Y off http://localhost:9090/status -O /dev/null > /dev/null 2>&1"
interval: 10s

1063
dev/postman.json vendored

File diff suppressed because it is too large Load Diff

View File

@ -7,9 +7,15 @@
"value": "http://127.0.0.1:8080",
"description": "",
"enabled": true
},
{
"key": "api_key",
"value": "demosecret123",
"description": "",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2018-11-17T16:55:15.031Z",
"_postman_exported_using": "Postman/6.5.2"
}
}

20
dev/pwd-stack.yml vendored Normal file
View File

@ -0,0 +1,20 @@
version: '3.1'
services:
statping:
hostname: statping
image: statping/statping
ports:
- 8080:8080
environment:
PORT: 8080
SERVICES: '[{"name": "Local Statping", "type": "http", "domain": "http://localhost:8585", "interval": 30}]'
DB_CONN: sqlite
API_KEY: exampleapikey
API_SECRET: exampleapisecret
NAME: Statping on SQLite
DOMAIN: http://localhost:8080
DESCRIPTION: This is a dev environment on SQLite!
ADMIN_USER: admin
ADMIN_PASS: admin

View File

@ -11,7 +11,7 @@
"cypress:open": "cypress open",
"cypress:test": "cypress run --browser chrome --record false --key $CYPRESS_KEY",
"test": "start-server-and-test start http://localhost:8080/api cypress:test",
"start": "statping -port 8080"
"start": "statping --port 8080 > /dev/null"
},
"dependencies": {
"@fortawesome/fontawesome-free-solid": "^5.1.0-3",

View File

@ -12,6 +12,11 @@ class Api {
}
async oauth() {
const oauth = axios.get('api/oauth').then(response => (response.data))
return oauth
}
async core() {
const core = axios.get('api').then(response => (response.data))
if (core.allow_reports) {
@ -198,7 +203,7 @@ class Api {
}
async logs() {
return axios.get('api/logs').then(response => (response.data))
return axios.get('api/logs').then(response => (response.data)) || []
}
async logs_last() {

View File

@ -1,5 +1,5 @@
<template>
<button v-html="loading ? loadLabel : label" @click.prevent="runAction" type="submit" :disabled="loading || disabled" class="btn btn-block" :class="{class: !loading, 'btn-outline-light': loading}">
<button v-html="loading ? loadLabel : label" @click.prevent="runAction" type="submit" :disabled="loading || disabled" class="btn btn-block" :class="{'btn-outline-light': loading}">
</button>
</template>
@ -15,10 +15,6 @@
type: String,
required: true
},
class: {
type: String,
default: "btn-primary"
},
disabled: {
type: Boolean,
default: false

View File

@ -13,7 +13,6 @@
</template>
<script>
import Api from "../../API";
import MiniSparkLine from './MiniSparkLine';
import ServiceSparkLine from './ServiceSparkLine';
@ -42,21 +41,6 @@
this.chart = this.convertToChartData(this.func.chart);
}
},
async latencyYesterday() {
const todayTime = await Api.service_hits(this.service.id, this.toUnix(this.nowSubtract(86400)), this.toUnix(new Date()), this.group, false)
const fetched = await Api.service_hits(this.service.id, this.start, this.end, this.group, false)
let todayAmount = this.addAmounts(todayTime)
let yesterday = this.addAmounts(fetched)
},
addAmounts(data) {
let total = 0
data.forEach((f) => {
total += parseInt(f.amount)
});
return total
}
}
</script>

View File

@ -104,7 +104,6 @@
import ServiceFailures from './ServiceFailures';
import ServiceSparkLine from "./ServiceSparkLine";
import Api from "../../API";
import StatsGen from "./StatsGen";
export default {
name: 'ServiceInfo',
@ -113,7 +112,6 @@
ServiceFailures,
FormIncident,
FormMessage,
StatsGen,
ServiceSparkLine
},
props: {

View File

@ -1,75 +0,0 @@
<template>
<div class="col-3 text-left">
<span class="text-success font-5 font-weight-bold">{{value}}</span>
<span class="font-2 d-block">{{title}}</span>
</div>
</template>
<script>
import Api from "../../API";
export default {
name: 'StatsGen',
props: {
service: {
type: Object,
required: true
},
title: {
type: String,
required: true
},
start: {
type: Number,
required: true
},
end: {
type: Number,
required: true
},
group: {
type: String,
required: true
},
expression: {
type: String,
required: true
},
in_value: {
required: false
}
},
data() {
return {
value: "+17%"
}
},
async mounted() {
if (this.in_value) {
this.value = this.in_value
}
await this.latencyYesterday();
},
methods: {
async latencyYesterday() {
const todayTime = await Api.service_hits(this.service.id, this.toUnix(this.nowSubtract(86400)), this.toUnix(new Date()), this.group, false)
const fetched = await Api.service_hits(this.service.id, this.start, this.end, this.group, false)
let todayAmount = this.addAmounts(todayTime)
let yesterday = this.addAmounts(fetched)
},
addAmounts(data) {
let total = 0
data.forEach((f) => {
total += parseInt(f.amount)
});
return total
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -1,4 +1,5 @@
<template>
<div>
<form @submit.prevent="login" autocomplete="on">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">Username</label>
@ -22,8 +23,21 @@
</button>
</div>
</div>
</form>
<a v-if="oauth.gh_client_id" :href="GHlogin()" class="btn btn-block">
Github Login
</a>
<a v-if="oauth.slack_client_id" :href="Slacklogin()" class="btn btn-block">
Slack Login
</a>
<a v-if="oauth.google_client_id" :href="Googlelogin()" class="btn btn-block">
Google Login
</a>
</div>
</template>
<script>
@ -32,12 +46,12 @@
export default {
name: 'FormLogin',
computed: {
core() {
return this.$store.getters.core
},
oauth() {
return this.$store.getters.core.oauth
}
core() {
return this.$store.getters.core
},
oauth() {
return this.$store.getters.oauth
}
},
data() {
return {
@ -51,9 +65,6 @@
slack_scope: "identity.email,identity.basic"
}
},
mounted() {
this.GHlogin()
},
methods: {
checkForm() {
if (!this.username || !this.password) {
@ -76,10 +87,15 @@
}
this.loading = false
},
async GHlogin() {
const core = this.$store.getters.core;
this.ghLoginURL = `https://github.com/login/oauth/authorize?client_id=${core.gh_client_id}&redirect_uri=${core.domain}/oauth/callback&scope=user,repo`
}
GHlogin() {
return `https://github.com/login/oauth/authorize?client_id=${this.oauth.gh_client_id}&redirect_uri=${this.core.domain}/api/oauth/github&scope=user,repo`
},
Slacklogin() {
return `https://slack.com/oauth/authorize?client_id=${this.oauth.slack_client_id}&redirect_uri=${this.core.domain}/api/oauth/slack&scope=users.profile:read,users:read.email`
},
Googlelogin() {
return `https://accounts.google.com/signin/oauth?client_id=${this.oauth.google_client_id}&redirect_uri=${this.core.domain}/api/oauth/google&response_type=code&scope=https://www.googleapis.com/auth/userinfo.profile+https://www.googleapis.com/auth/userinfo.email`
}
}
}
</script>

View File

@ -1,13 +1,14 @@
<template>
<form @submit.prevent="saveOAuth">
{{core.oauth}}
<div class="card text-black-50 bg-white mb-3">
<div class="card-header">Internal Login</div>
<div class="card-body">
<div class="form-group row">
<label for="switch-gh-oauth" class="col-sm-4 col-form-label">OAuth Login Settings</label>
<div class="col-md-8 col-xs-12 mt-1">
<span @click="oauth.internal_enabled = !!core.oauth.internal_enabled" class="switch float-left">
<input v-model="oauth.internal_enabled" type="checkbox" class="switch" id="switch-local-oauth" :checked="oauth.internal_enabled">
<span @click="local_enabled = !!local_enabled" class="switch float-left">
<input v-model="local_enabled" type="checkbox" class="switch" id="switch-local-oauth" :checked="local_enabled">
<label for="switch-local-oauth">Use email/password Authentication</label>
</span>
</div>
@ -15,7 +16,7 @@
<div class="form-group row">
<label for="whitelist_domains" class="col-sm-4 col-form-label">Whitelist Domains</label>
<div class="col-sm-8">
<input v-model="oauth.oauth.oauth_domains" type="text" class="form-control" placeholder="domain.com" id="whitelist_domains">
<input v-model="oauth.oauth_domains" type="text" class="form-control" placeholder="domain.com" id="whitelist_domains">
</div>
</div>
</div>
@ -28,20 +29,20 @@
<div class="form-group row mt-3">
<label for="github_client" class="col-sm-4 col-form-label">Github Client ID</label>
<div class="col-sm-8">
<input v-model="oauth.oauth.gh_client_id" type="text" class="form-control" id="github_client" required>
<input v-model="oauth.gh_client_id" type="text" class="form-control" id="github_client" required>
</div>
</div>
<div class="form-group row">
<label for="github_secret" class="col-sm-4 col-form-label">Github Client Secret</label>
<div class="col-sm-8">
<input v-model="oauth.oauth.gh_client_secret" type="text" class="form-control" id="github_secret" required>
<input v-model="oauth.gh_client_secret" type="text" class="form-control" id="github_secret" required>
</div>
</div>
<div class="form-group row">
<label for="switch-gh-oauth" class="col-sm-4 col-form-label">Enable Github Login</label>
<div class="col-md-8 col-xs-12 mt-1">
<span @click="oauth.github_enabled = !!oauth.github_enabled" class="switch float-left">
<input v-model="oauth.github_enabled" type="checkbox" class="switch" id="switch-gh-oauth" :checked="oauth.github_enabled">
<span @click="github_enabled = !!github_enabled" class="switch float-left">
<input v-model="github_enabled" type="checkbox" class="switch" id="switch-gh-oauth" :checked="github_enabled">
<label for="switch-gh-oauth"> </label>
</span>
</div>
@ -50,7 +51,7 @@
<label for="gh_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8">
<div class="input-group">
<input v-bind:value="`${core.domain}/oauth/github`" type="text" class="form-control" id="gh_callback" readonly>
<input v-bind:value="`${core.domain}/api/oauth/github`" type="text" class="form-control" id="gh_callback" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/oauth/github`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
@ -89,7 +90,7 @@
<label for="google_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8">
<div class="input-group">
<input v-bind:value="`${core.domain}/oauth/google`" type="text" class="form-control" id="google_callback" readonly>
<input v-bind:value="`${core.domain}/api/oauth/google`" type="text" class="form-control" id="google_callback" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/oauth/google`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
@ -126,7 +127,7 @@
<label for="switch-slack-oauth" class="col-sm-4 col-form-label">Enable Slack Login</label>
<div class="col-md-8 col-xs-12 mt-1">
<span @click="slack_enabled = !!slack_enabled" class="switch float-left">
<input v-model="slack_enabled" type="checkbox" class="switch" id="switch-slack-oauth" :checked="google_enabled">
<input v-model="slack_enabled" type="checkbox" class="switch" id="switch-slack-oauth" :checked="slack_enabled">
<label for="switch-slack-oauth"> </label>
</span>
</div>
@ -135,7 +136,7 @@
<label for="slack_callback" class="col-sm-4 col-form-label">Callback URL</label>
<div class="col-sm-8">
<div class="input-group">
<input v-bind:value="`${core.domain}/oauth/slack`" type="text" class="form-control" id="slack_callback" readonly>
<input v-bind:value="`${core.domain}/api/oauth/slack`" type="text" class="form-control" id="slack_callback" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy(`${core.domain}/oauth/slack`)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>
@ -158,56 +159,58 @@
export default {
name: 'OAuth',
computed: {
oauth() {
return this.$store.getters.core.oauth
}
core() {
return this.$store.getters.core
},
oauth() {
return this.$store.getters.oauth
}
},
data() {
return {
internal_enabled: this.has('local'),
google_enabled: this.has('google'),
github_enabled: this.has('github'),
slack_enabled: this.has('slack')
google_enabled: false,
slack_enabled: false,
github_enabled: false,
local_enabled: false
}
},
mounted() {
window.console.log(this.core.oauth)
},
beforeCreate() {
// this.github_enabled = this.$store.getters.core.oauth.oauth_providers.split(",").includes('github')
// const c = await Api.core()
// this.auth = c.auth
this.local_enabled = this.has('local')
this.github_enabled = this.has('github')
this.google_enabled = this.has('google')
this.slack_enabled = this.has('slack')
},
methods: {
providers() {
let providers = [];
if (this.github_enabled) {
providers.push("github")
}
if (this.local_enabled) {
providers.push("local")
}
if (this.google_enabled) {
providers.push("google")
}
if (this.slack_enabled) {
providers.push("slack")
}
return providers.join(",")
},
has(val) {
if (!this.core.oauth.oauth_providers) {
if (!this.oauth.oauth_providers) {
return false
}
return this.core.oauth.oauth_providers.split(",").includes(val)
},
providers() {
let providers = [];
if (this.github_enabled) {
providers.push("github")
}
if (this.internal_enabled) {
providers.push("local")
}
if (this.google_enabled) {
providers.push("google")
}
if (this.slack_enabled) {
providers.push("slack")
}
return providers.join(",")
return this.oauth.oauth_providers.split(",").includes(val)
},
async saveOAuth() {
let c = this.$store.getters.core
let c = this.core
c.oauth = this.oauth
c.oauth.oauth_providers = this.providers()
await Api.core_save(c)
const core = await Api.core()
this.$store.commit('setCore', core)
this.$store.commit('setOAuth', c.oauth)
}
}
}

View File

@ -135,6 +135,16 @@
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Follow HTTP Redirects</label>
<div class="col-8 mt-1">
<span @click="service.redirect = !!service.redirect" class="switch float-left">
<input v-model="service.redirect" type="checkbox" name="redirect-option" class="switch" id="switch-redirect" v-bind:checked="service.redirect">
<label for="switch-redirect">Follow HTTP Redirects if server attempts</label>
</span>
</div>
</div>
<div v-if="service.type.match(/^(http)$/)" class="form-group row">
<label class="col-sm-4 col-form-label">Verify SSL</label>
<div class="col-8 mt-1">
@ -220,6 +230,7 @@
permalink: "",
order: 1,
verify_ssl: true,
redirect: true,
allow_notifications: true,
notify_all_changes: true,
notify_after: 2,

View File

@ -20,6 +20,7 @@ export default new Vuex.Store({
hasAllData: false,
hasPublicData: false,
core: {},
oauth: {},
token: null,
services: [],
service: null,
@ -35,6 +36,7 @@ export default new Vuex.Store({
hasAllData: state => state.hasAllData,
hasPublicData: state => state.hasPublicData,
core: state => state.core,
oauth: state => state.oauth,
token: state => state.token,
services: state => state.services,
service: state => state.service,
@ -130,9 +132,12 @@ export default new Vuex.Store({
setAdmin (state, admin) {
state.admin = admin
},
setUser (state, user) {
state.user = user
},
setUser (state, user) {
state.user = user
},
setOAuth (state, oauth) {
state.oauth = oauth
},
},
actions: {
async getAllServices(context) {
@ -153,8 +158,9 @@ export default new Vuex.Store({
context.commit("setServices", services);
const messages = await Api.messages()
context.commit("setMessages", messages)
const oauth = await Api.oauth()
context.commit("setOAuth", oauth);
context.commit("setHasPublicData", true)
window.console.log('finished loading required data')
},
async loadAdmin(context) {
const groups = await Api.groups()
@ -168,8 +174,10 @@ export default new Vuex.Store({
context.commit("setHasPublicData", true)
const notifiers = await Api.notifiers()
context.commit("setNotifiers", notifiers);
const users = await Api.users()
context.commit("setUsers", users);
const users = await Api.users()
context.commit("setUsers", users);
const oauth = await Api.oauth()
context.commit("setOAuth", oauth);
}
}
});

15
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/ararog/timeago v0.0.0-20160328174124-e9969cf18b8d
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/getsentry/sentry-go v0.5.1
github.com/go-mail/mail v2.3.1+incompatible
github.com/gogo/protobuf v1.3.1 // indirect
@ -17,19 +18,31 @@ require (
github.com/jinzhu/gorm v1.9.12
github.com/joho/godotenv v1.3.0
github.com/kataras/iris/v12 v12.0.1
github.com/mattn/go-sqlite3 v2.0.1+incompatible
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mitchellh/mapstructure v1.2.2 // indirect
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/pkg/errors v0.9.1
github.com/prometheus/common v0.9.1
github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f
github.com/russross/blackfriday/v2 v2.0.1
github.com/sirupsen/logrus v1.4.2
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.0.0
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.3
github.com/stretchr/testify v1.5.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
golang.org/x/crypto v0.0.0-20200320181102-891825fb96df
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect
golang.org/x/text v0.3.2 // indirect
golang.org/x/tools v0.0.0-20200321014904-268ba720d32c // indirect
google.golang.org/grpc v1.28.0
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.55.0 // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v2 v2.2.8

92
go.sum
View File

@ -11,6 +11,7 @@ github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voi
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 h1:WDC6ySpJzbxGWFh4aMxFFC28wwGp5pEuoTtvA4q/qQ4=
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
@ -29,13 +30,19 @@ github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
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/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -47,6 +54,7 @@ github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6ps
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -59,10 +67,14 @@ github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/getsentry/sentry-go v0.5.1 h1:MIPe7ScHADsrK2vznqmhksIUFxq7m0JfTh+ZIMkI+VQ=
github.com/getsentry/sentry-go v0.5.1/go.mod h1:B8H7x8TYDPkeWPRzGpIiFO97LZP6rL8A3hEt8lUItMw=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
@ -83,11 +95,13 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
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=
@ -97,10 +111,12 @@ github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaW
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
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.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
@ -109,10 +125,15 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4=
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
@ -131,8 +152,10 @@ github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
@ -146,6 +169,7 @@ github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/l
github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d h1:V5Rs9ztEWdp58oayPq/ulmlqJJZeJP6pP79uP3qjcao=
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
@ -163,22 +187,32 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
@ -188,10 +222,14 @@ github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -201,18 +239,24 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f h1:onGP+qmYmjKs7pkmi9j0mwyr97/D5wki80e74aKIOxg=
github.com/rendon/testcli v0.0.0-20161027181003-6283090d169f/go.mod h1:cq57a4l475CeMvE7RRpSui1MEqCmhirIt1E7kl8BC2Q=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
@ -224,14 +268,36 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
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/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
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.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@ -239,8 +305,11 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e h1:nt2877sKfojlHCTOBXbpWjBkuWKritFaGIfgQwbQUls=
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e/go.mod h1:B4+Kq1u5FlULTjFSM707Q6e/cOHFv0z/6QRoxubDIQ8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
@ -255,12 +324,17 @@ github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -285,6 +359,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
@ -301,6 +376,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -310,8 +386,16 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HXxFpW2sdCiP8MJSKLQY=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -334,6 +418,7 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/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.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4=
@ -344,15 +429,22 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gG
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ=
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,11 +1,10 @@
package handlers
import (
"encoding/json"
"errors"
"fmt"
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/groups"
"github.com/statping/statping/types/incidents"
"github.com/statping/statping/types/messages"
@ -22,7 +21,7 @@ type apiResponse struct {
Status string `json:"status"`
Object string `json:"type,omitempty"`
Method string `json:"method,omitempty"`
Error string `json:"error,omitempty"`
Error error `json:"error,omitempty"`
Id int64 `json:"id,omitempty"`
Output interface{} `json:"output,omitempty"`
}
@ -52,10 +51,14 @@ func apiRenewHandler(w http.ResponseWriter, r *http.Request) {
returnJson(output, w, r)
}
func apiOAuthHandler(r *http.Request) interface{} {
app := core.App
return app.OAuth
}
func apiCoreHandler(w http.ResponseWriter, r *http.Request) {
var c *core.Core
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&c)
err := DecodeJSON(r, &c)
if err != nil {
sendErrorJson(err, w, r)
return
@ -111,17 +114,18 @@ func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
returnJson(output, w, r)
}
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request, statusCode ...int) {
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) {
errCode := 0
e, ok := err.(errors.Error)
if ok {
errCode = e.Status()
}
log.WithField("url", r.URL.String()).
WithField("method", r.Method).
WithField("code", statusCode).
WithField("code", errCode).
Errorln(fmt.Errorf("sending error response for %s: %s", r.URL.String(), err.Error()))
output := apiResponse{
Status: "error",
Error: err.Error(),
}
returnJson(output, w, r, statusCode...)
returnJson(err, w, r)
}
func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *http.Request) {
@ -173,11 +177,6 @@ func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *ht
}
func sendUnauthorizedJson(w http.ResponseWriter, r *http.Request) {
output := apiResponse{
Status: "error",
Error: errors.New("not authorized").Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
returnJson(output, w, r)
returnJson(errors.NotAuthenticated, w, r)
}

View File

@ -1,6 +1,7 @@
package handlers
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/getsentry/sentry-go"
@ -18,7 +19,6 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
)
@ -34,13 +34,6 @@ func init() {
core.New("test")
}
//func TestResetDatabase(t *testing.T) {
// err := core.TmpRecords("handlers.db")
// t.Log(err)
// require.Nil(t, err)
// require.NotNil(t, core.CoreApp)
//}
func TestFailedHTTPServer(t *testing.T) {
err := RunHTTPServer("missinghost", 0)
assert.Error(t, err)
@ -48,12 +41,12 @@ func TestFailedHTTPServer(t *testing.T) {
func TestSetupRoutes(t *testing.T) {
form := url.Values{}
form.Add("db_host", "")
form.Add("db_user", "")
form.Add("db_password", "")
form.Add("db_database", "")
form.Add("db_connection", "sqlite")
form.Add("db_port", "")
form.Add("db_host", utils.Params.GetString("DB_HOST"))
form.Add("db_user", utils.Params.GetString("DB_USER"))
form.Add("db_password", utils.Params.GetString("DB_PASS"))
form.Add("db_database", utils.Params.GetString("DB_DATABASE"))
form.Add("db_connection", utils.Params.GetString("DB_CONN"))
form.Add("db_port", utils.Params.GetString("DB_PORT"))
form.Add("project", "Tester")
form.Add("username", "admin")
form.Add("password", "password123")
@ -62,6 +55,21 @@ func TestSetupRoutes(t *testing.T) {
form.Add("domain", "http://localhost:8080")
form.Add("email", "info@statping.com")
badForm := url.Values{}
badForm.Add("db_host", "badconnection")
badForm.Add("db_user", utils.Params.GetString("DB_USER"))
badForm.Add("db_password", utils.Params.GetString("DB_PASS"))
badForm.Add("db_database", utils.Params.GetString("DB_DATABASE"))
badForm.Add("db_connection", "mysql")
badForm.Add("db_port", utils.Params.GetString("DB_PORT"))
badForm.Add("project", "Tester")
badForm.Add("username", "admin")
badForm.Add("password", "password123")
badForm.Add("sample_data", "on")
badForm.Add("description", "This is an awesome test")
badForm.Add("domain", "http://localhost:8080")
badForm.Add("email", "info@statping.com")
tests := []HTTPTest{
{
Name: "Statping Check",
@ -76,13 +84,22 @@ func TestSetupRoutes(t *testing.T) {
},
},
{
Name: "Statping Run Setup",
URL: "/api/setup",
Method: "POST",
Body: form.Encode(),
ExpectedStatus: 200,
HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"},
ExpectedFiles: []string{dir + "/config.yml", dir + "/" + "statping.db"},
Name: "Statping Error Setup",
URL: "/api/setup",
Method: "POST",
Body: badForm.Encode(),
ExpectedStatus: 500,
ExpectedContains: []string{BadJSONDatabase},
HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"},
},
{
Name: "Statping Run Setup",
URL: "/api/setup",
Method: "POST",
Body: form.Encode(),
//ExpectedStatus: 200,
HttpHeaders: []string{"Content-Type=application/x-www-form-urlencoded"},
ExpectedFiles: []string{utils.Directory + "/config.yml"},
FuncTest: func(t *testing.T) error {
if !core.App.Setup {
return errors.New("core has not been setup")
@ -99,7 +116,8 @@ func TestSetupRoutes(t *testing.T) {
return nil
},
AfterTest: StopServices,
}}
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
@ -113,6 +131,7 @@ func TestSetupRoutes(t *testing.T) {
}
func TestMainApiRoutes(t *testing.T) {
date := utils.Now().Format("2006-01")
tests := []HTTPTest{
{
Name: "Statping Details",
@ -135,6 +154,15 @@ func TestMainApiRoutes(t *testing.T) {
BeforeTest: SetTestENV,
SecureRoute: true,
},
{
Name: "Statping View Cache",
URL: "/api/cache",
Method: "GET",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
SecureRoute: true,
ResponseLen: 0,
},
{
Name: "Statping Clear Cache",
URL: "/api/clear_cache",
@ -172,18 +200,48 @@ func TestMainApiRoutes(t *testing.T) {
Method: "GET",
ExpectedStatus: 404,
},
//{
// Name: "Prometheus Export Metrics",
// URL: "/metrics",
// Method: "GET",
// BeforeTest: SetTestENV,
// ExpectedStatus: 200,
// ExpectedContains: []string{
// `Statping Totals`,
// `total_failures`,
// `Golang Metrics`,
// },
//},
{
Name: "Health Check endpoint",
URL: "/health",
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{`"online":true`, `"setup":true`},
},
{
Name: "Logs endpoint",
URL: "/api/logs",
Method: "GET",
ExpectedStatus: 200,
GreaterThan: 20,
ExpectedContains: []string{date},
},
{
Name: "Logs endpoint",
URL: "/api/logs",
Method: "GET",
ExpectedStatus: 200,
GreaterThan: 20,
ExpectedContains: []string{date},
},
{
Name: "Logs Last Line endpoint",
URL: "/api/logs/last",
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{date},
},
{
Name: "Prometheus Export Metrics",
URL: "/metrics",
Method: "GET",
BeforeTest: SetTestENV,
ExpectedStatus: 200,
ExpectedContains: []string{
`Statping Totals`,
`total_failures`,
`Golang Metrics`,
},
},
}
for _, v := range tests {
@ -197,6 +255,8 @@ func TestMainApiRoutes(t *testing.T) {
type HttpFuncTest func(*testing.T) error
type ResponseFunc func(*testing.T, []byte) error
// HTTPTest contains all the parameters for a HTTP Unit Test
type HTTPTest struct {
Name string
@ -211,8 +271,11 @@ type HTTPTest struct {
FuncTest HttpFuncTest
BeforeTest HttpFuncTest
AfterTest HttpFuncTest
ResponseFunc ResponseFunc
ResponseLen int
GreaterThan int
SecureRoute bool
Skip bool
}
func logTest(t *testing.T, err error) error {
@ -228,6 +291,9 @@ func logTest(t *testing.T, err error) error {
// RunHTTPTest accepts a HTTPTest type to execute the HTTP request
func RunHTTPTest(test HTTPTest, t *testing.T) (string, *testing.T, error) {
if test.Skip {
t.SkipNow()
}
if test.BeforeTest != nil {
if err := test.BeforeTest(t); err != nil {
return "", t, logTest(t, err)
@ -240,6 +306,13 @@ func RunHTTPTest(test HTTPTest, t *testing.T) (string, *testing.T, error) {
}
defer rr.Result().Body.Close()
if test.ExpectedStatus != 0 {
if test.ExpectedStatus != rr.Result().StatusCode {
assert.Equal(t, test.ExpectedStatus, rr.Result().StatusCode)
return "", t, fmt.Errorf("status code %v does not match %v", rr.Result().StatusCode, test.ExpectedStatus)
}
}
body, err := ioutil.ReadAll(rr.Result().Body)
if err != nil {
assert.Nil(t, err)
@ -248,10 +321,6 @@ func RunHTTPTest(test HTTPTest, t *testing.T) (string, *testing.T, error) {
stringBody := string(body)
if test.ExpectedStatus != rr.Result().StatusCode {
assert.Equal(t, test.ExpectedStatus, rr.Result().StatusCode)
return stringBody, t, fmt.Errorf("status code %v does not match %v", rr.Result().StatusCode, test.ExpectedStatus)
}
if len(test.ExpectedContains) != 0 {
for _, v := range test.ExpectedContains {
assert.Contains(t, stringBody, v)
@ -271,21 +340,24 @@ func RunHTTPTest(test HTTPTest, t *testing.T) (string, *testing.T, error) {
err := test.FuncTest(t)
assert.Nil(t, err)
}
if test.ResponseFunc != nil {
err := test.ResponseFunc(t, body)
assert.Nil(t, err)
}
if test.ResponseLen != 0 {
var respArray []interface{}
err := json.Unmarshal(body, &respArray)
assert.Nil(t, err)
assert.Equal(t, test.ResponseLen, len(respArray))
}
//if test.SecureRoute {
// UnsetTestENV()
// rec, err := Request(test)
// if err != nil {
// return "", t, logTest(t, err)
// }
// defer rec.Result().Body.Close()
// assert.Equal(t, http.StatusUnauthorized, rec.Result().StatusCode)
//}
if test.GreaterThan != 0 {
var respArray []interface{}
err := json.Unmarshal(body, &respArray)
assert.Nil(t, err)
assert.GreaterOrEqual(t, len(respArray), test.GreaterThan)
}
if test.AfterTest != nil {
if err := test.AfterTest(t); err != nil {
@ -312,11 +384,13 @@ func Request(test HTTPTest) (*httptest.ResponseRecorder, error) {
}
func SetTestENV(t *testing.T) error {
return os.Setenv("GO_ENV", "test")
utils.Params.Set("GO_ENV", "test")
return nil
}
func UnsetTestENV(t *testing.T) error {
return os.Setenv("GO_ENV", "production")
utils.Params.Set("GO_ENV", "production")
return nil
}
func StopServices(t *testing.T) error {
@ -325,3 +399,20 @@ func StopServices(t *testing.T) error {
}
return nil
}
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
var (
Success = `"status":"success"`
MethodCreate = `"method":"create"`
MethodUpdate = `"method":"update"`
MethodDelete = `"method":"delete"`
BadJSON = `{incorrect: JSON %%% formatting, [&]}`
BadJSONResponse = `{"error":"could not decode incoming JSON"}`
BadJSONDatabase = `{"error":"error connecting to database`
)

View File

@ -1,26 +1,37 @@
package handlers
import (
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/services"
"github.com/statping/statping/utils"
"net"
"net/http"
)
func findCheckin(r *http.Request) (*checkins.Checkin, string, error) {
vars := mux.Vars(r)
id := vars["api"]
if id == "" {
return nil, "", errors.IDMissing
}
checkin, err := checkins.FindByAPI(id)
if err != nil {
return nil, id, errors.Missing(checkins.Checkin{}, id)
}
return checkin, id, nil
}
func apiAllCheckinsHandler(w http.ResponseWriter, r *http.Request) {
chks := checkins.All()
returnJson(chks, w, r)
}
func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
checkin, err := checkins.FindByAPI(vars["api"])
checkin, _, err := findCheckin(r)
if err != nil {
sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r)
sendErrorJson(err, w, r)
return
}
returnJson(checkin, w, r)
@ -28,15 +39,14 @@ func apiCheckinHandler(w http.ResponseWriter, r *http.Request) {
func checkinCreateHandler(w http.ResponseWriter, r *http.Request) {
var checkin *checkins.Checkin
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&checkin)
err := DecodeJSON(r, &checkin)
if err != nil {
sendErrorJson(err, w, r)
return
}
service, err := services.Find(checkin.ServiceId)
if err != nil {
sendErrorJson(fmt.Errorf("missing service_id field"), w, r)
sendErrorJson(err, w, r)
return
}
checkin.ServiceId = service.Id
@ -48,10 +58,9 @@ func checkinCreateHandler(w http.ResponseWriter, r *http.Request) {
}
func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
checkin, err := checkins.FindByAPI(vars["api"])
checkin, _, err := findCheckin(r)
if err != nil {
sendErrorJson(fmt.Errorf("checkin %s was not found", vars["api"]), w, r)
sendErrorJson(err, w, r)
return
}
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
@ -65,7 +74,7 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
err = hit.Create()
if err != nil {
sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r)
sendErrorJson(err, w, r)
return
}
checkin.Failing = false
@ -74,10 +83,9 @@ func checkinHitHandler(w http.ResponseWriter, r *http.Request) {
}
func checkinDeleteHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
checkin, err := checkins.FindByAPI(vars["api"])
checkin, _, err := findCheckin(r)
if err != nil {
sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r)
sendErrorJson(err, w, r)
return
}

View File

@ -5,30 +5,106 @@ import (
"testing"
)
func TestApiCheckinRoutes(t *testing.T) {
func TestUnAuthenticatedCheckinRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "Statping Checkins",
Name: "No Authentication - New Checkin",
URL: "/api/checkins",
Method: "GET",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
SecureRoute: true,
}, {
Name: "Statping Create Checkin",
URL: "/api/checkins",
Method: "POST",
Body: `{
"service_id": 2,
"name": "Server Checkin",
"interval": 900,
"grace": 60
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success","type":"checkin","method":"create"`},
BeforeTest: SetTestENV,
SecureRoute: true,
}}
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Delete Checkin",
URL: "/api/checkins/1",
Method: "DELETE",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
str, t, err := RunHTTPTest(v, t)
t.Logf("Test %s: \n %v\n", v.Name, str)
assert.Nil(t, err)
})
}
}
func TestApiCheckinRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "Statping Create Checkins",
URL: "/api/checkins",
Method: "POST",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
SecureRoute: true,
ExpectedContains: []string{Success},
Body: `{
"name": "Example Checkin",
"service_id": 1,
"checkin_interval": 300,
"grace_period": 60,
"api_key": "example"
}`,
},
{
Name: "Statping Checkins",
URL: "/api/checkins",
Method: "GET",
ExpectedStatus: 200,
ResponseLen: 3,
BeforeTest: SetTestENV,
},
{
Name: "Statping View Checkin",
URL: "/api/checkins/example",
Method: "GET",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
SecureRoute: true,
},
{
Name: "Statping Trigger Checkin",
URL: "/checkin/example",
Method: "GET",
ExpectedStatus: 200,
SecureRoute: true,
BeforeTest: SetTestENV,
},
{
Name: "Statping Missing Trigger Checkin",
URL: "/checkin/missing123",
Method: "GET",
BeforeTest: SetTestENV,
ExpectedStatus: 404,
},
{
Name: "Statping Missing Checkin",
URL: "/api/checkins/missing123",
Method: "GET",
BeforeTest: SetTestENV,
ExpectedStatus: 404,
},
{
Name: "Statping Delete Checkin",
URL: "/api/checkins/example",
Method: "DELETE",
BeforeTest: SetTestENV,
ExpectedContains: []string{Success},
ExpectedStatus: 200,
},
{
Name: "Incorrect JSON POST",
URL: "/api/checkins",
Body: BadJSON,
ExpectedContains: []string{BadJSONResponse},
Method: "POST",
ExpectedStatus: 422,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {

View File

@ -17,15 +17,6 @@ func logoutHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, basePath, http.StatusSeeOther)
}
func helpHandler(w http.ResponseWriter, r *http.Request) {
if !IsUser(r) {
http.Redirect(w, r, basePath, http.StatusSeeOther)
return
}
help := source.HelpMarkdown()
ExecuteResponse(w, r, "help.gohtml", help, nil)
}
func logsHandler(w http.ResponseWriter, r *http.Request) {
utils.LockLines.Lock()
logs := make([]string, 0)
@ -101,7 +92,7 @@ func apiThemeSaveHandler(w http.ResponseWriter, r *http.Request) {
}
func apiThemeCreateHandler(w http.ResponseWriter, r *http.Request) {
dir := utils.Directory
dir := utils.Params.GetString("STATPING_DIR")
utils.Log.Infof("creating assets in folder: %s/%s", dir, "assets")
if err := source.CreateAllAssets(dir); err != nil {
log.Errorln(err)
@ -194,6 +185,7 @@ func apiLoginHandler(w http.ResponseWriter, r *http.Request) {
form := parseForm(r)
username := form.Get("username")
password := form.Get("password")
user, auth := users.AuthUser(username, password)
if auth {
utils.Log.Infoln(fmt.Sprintf("User %v logged in from IP %v", user.Username, r.RemoteAddr))

View File

@ -1,49 +0,0 @@
package handlers
import (
"encoding/json"
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/groups"
"github.com/statping/statping/types/messages"
"github.com/statping/statping/types/services"
"github.com/statping/statping/types/users"
)
// ExportChartsJs renders the charts for the index page
type ExportData struct {
Core *core.Core `json:"core"`
Services []services.Service `json:"services"`
Messages []*messages.Message `json:"messages"`
Checkins []*checkins.Checkin `json:"checkins"`
Users []*users.User `json:"users"`
Groups []*groups.Group `json:"groups"`
Notifiers []core.AllNotifiers `json:"notifiers"`
}
// ExportSettings will export a JSON file containing all of the settings below:
// - Core
// - Notifiers
// - Checkins
// - Users
// - Services
// - Groups
// - Messages
func ExportSettings() ([]byte, error) {
c, err := core.Select()
if err != nil {
return nil, err
}
data := ExportData{
Core: c,
//Notifiers: notifications.All(),
Checkins: checkins.All(),
Users: users.All(),
Services: services.AllInOrder(),
Groups: groups.All(),
Messages: messages.All(),
}
export, err := json.Marshal(data)
return export, err
}

View File

@ -2,19 +2,30 @@ package handlers
import (
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/groups"
"github.com/statping/statping/utils"
"net/http"
)
func selectGroup(r *http.Request) (*groups.Group, error) {
func findGroup(r *http.Request) (*groups.Group, error) {
vars := mux.Vars(r)
if utils.NotNumber(vars["id"]) {
return nil, errors.NotNumber
}
id := utils.ToInt(vars["id"])
if id == 0 {
return nil, errors.IDMissing
}
g, err := groups.Find(id)
if err != nil {
return nil, err
}
if !g.Public.Bool {
if !IsReadAuthenticated(r) {
return nil, errors.NotAuthenticated
}
}
return g, nil
}
@ -26,9 +37,9 @@ func apiAllGroupHandler(r *http.Request) interface{} {
// apiGroupHandler will show a single group
func apiGroupHandler(w http.ResponseWriter, r *http.Request) {
group, err := selectGroup(r)
group, err := findGroup(r)
if err != nil {
sendErrorJson(errors.Wrap(err, "group not found"), w, r, http.StatusNotFound)
sendErrorJson(err, w, r)
return
}
returnJson(group, w, r)
@ -36,10 +47,9 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request) {
// apiGroupUpdateHandler will update a group
func apiGroupUpdateHandler(w http.ResponseWriter, r *http.Request) {
group, err := selectGroup(r)
group, err := findGroup(r)
if err != nil {
w.WriteHeader(http.StatusNotFound)
sendErrorJson(errors.Wrap(err, "group not found"), w, r)
sendErrorJson(err, w, r)
return
}
@ -74,9 +84,9 @@ func apiCreateGroupHandler(w http.ResponseWriter, r *http.Request) {
// apiGroupDeleteHandler accepts a DELETE method to delete groups
func apiGroupDeleteHandler(w http.ResponseWriter, r *http.Request) {
group, err := selectGroup(r)
group, err := findGroup(r)
if err != nil {
sendErrorJson(errors.Wrap(err, "group not found"), w, r)
sendErrorJson(err, w, r)
return
}

View File

@ -1,10 +1,46 @@
package handlers
import (
"github.com/statping/statping/types/groups"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestUnAuthenticatedGroupRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "No Authentication - New Group",
URL: "/api/groups",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Update Group",
URL: "/api/groups/1",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Delete Group",
URL: "/api/groups/1",
Method: "DELETE",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
str, t, err := RunHTTPTest(v, t)
t.Logf("Test %s: \n %v\n", v.Name, str)
assert.Nil(t, err)
})
}
}
func TestGroupAPIRoutes(t *testing.T) {
tests := []HTTPTest{
{
@ -47,6 +83,14 @@ func TestGroupAPIRoutes(t *testing.T) {
Method: "POST",
ExpectedStatus: 200,
},
{
Name: "Incorrect JSON POST",
URL: "/api/groups",
Body: BadJSON,
ExpectedContains: []string{BadJSONResponse},
Method: "POST",
ExpectedStatus: 422,
},
{
Name: "Statping Public and Private Groups",
URL: "/api/groups",
@ -87,13 +131,34 @@ func TestGroupAPIRoutes(t *testing.T) {
ExpectedStatus: 404,
},
{
Name: "Statping Delete Group",
URL: "/api/groups/1",
Method: "DELETE",
ExpectedStatus: 200,
AfterTest: UnsetTestENV,
SecureRoute: true,
}}
Name: "Statping Update Group",
URL: "/api/groups/1",
Method: "POST",
Body: `{
"name": "Updated Group",
"public": false
}`,
ExpectedStatus: 200,
ExpectedContains: []string{Success, MethodUpdate},
BeforeTest: SetTestENV,
SecureRoute: true,
AfterTest: func(t *testing.T) error {
g, err := groups.Find(1)
require.Nil(t, err)
assert.Equal(t, "Updated Group", g.Name)
return nil
},
},
{
Name: "Statping Delete Group",
URL: "/api/groups/1",
Method: "DELETE",
ExpectedStatus: 200,
ExpectedContains: []string{Success, MethodDelete},
AfterTest: UnsetTestENV,
SecureRoute: true,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {

View File

@ -4,13 +4,12 @@ import (
"crypto/subtle"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/errors"
"html/template"
"net/http"
"os"
"path"
"strings"
"time"
@ -44,7 +43,7 @@ func RunHTTPServer(ip string, port int) error {
log.Infoln(fmt.Sprintf("Statping Secure HTTPS Server running on https://%v:%v", ip, 443))
usingSSL = true
} else {
log.Infoln("Statping HTTP Server running on http://" + host)
log.Infoln("Statping HTTP Server running on http://" + host + basePath)
}
router = Router()
@ -110,7 +109,7 @@ func IsReadAuthenticated(r *http.Request) bool {
// IsFullAuthenticated returns true if the HTTP request is authenticated. You can set the environment variable GO_ENV=test
// to bypass the admin authenticate to the dashboard features.
func IsFullAuthenticated(r *http.Request) bool {
if os.Getenv("GO_ENV") == "test" {
if utils.Params.Get("GO_ENV") == "test" {
return true
}
if core.App == nil {
@ -172,7 +171,7 @@ func IsAdmin(r *http.Request) bool {
if !core.App.Setup {
return false
}
if utils.Getenv("GO_ENV", false).(bool) {
if utils.Params.GetString("GO_ENV") == "test" {
return true
}
claim, err := getJwtToken(r)
@ -187,7 +186,7 @@ func IsUser(r *http.Request) bool {
if !core.App.Setup {
return false
}
if os.Getenv("GO_ENV") == "test" {
if utils.Params.Get("GO_ENV") == "test" {
return true
}
tk, err := getJwtToken(r)
@ -248,12 +247,19 @@ func ExecuteResponse(w http.ResponseWriter, r *http.Request, file string, data i
}
}
func returnJson(d interface{}, w http.ResponseWriter, r *http.Request, statusCode ...int) {
func returnJson(d interface{}, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if len(statusCode) != 0 {
code := statusCode[0]
w.WriteHeader(code)
if e, ok := d.(errors.Error); ok {
w.WriteHeader(e.Status())
json.NewEncoder(w).Encode(e)
return
}
if e, ok := d.(error); ok {
w.WriteHeader(500)
json.NewEncoder(w).Encode(errors.New(e.Error()))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(d)
}
@ -263,5 +269,5 @@ func error404Handler(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
}
w.WriteHeader(http.StatusNotFound)
ExecuteResponse(w, r, "index.html", nil, nil)
ExecuteResponse(w, r, "base.gohtml", nil, nil)
}

View File

@ -1,13 +1,29 @@
package handlers
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/incidents"
"github.com/statping/statping/utils"
"net/http"
)
func findIncident(r *http.Request) (*incidents.Incident, int64, error) {
vars := mux.Vars(r)
if utils.NotNumber(vars["id"]) {
return nil, 0, errors.NotNumber
}
id := utils.ToInt(vars["id"])
if id == 0 {
return nil, id, errors.IDMissing
}
checkin, err := incidents.Find(id)
if err != nil {
return nil, id, errors.Missing(&incidents.Incident{}, id)
}
return checkin, id, nil
}
func apiServiceIncidentsHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incids := incidents.FindByService(utils.ToInt(vars["id"]))
@ -15,8 +31,7 @@ func apiServiceIncidentsHandler(w http.ResponseWriter, r *http.Request) {
}
func apiIncidentUpdatesHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incid, err := incidents.Find(utils.ToInt(vars["id"]))
incid, _, err := findIncident(r)
if err != nil {
sendErrorJson(err, w, r)
return
@ -25,16 +40,19 @@ func apiIncidentUpdatesHandler(w http.ResponseWriter, r *http.Request) {
}
func apiCreateIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var update *incidents.IncidentUpdate
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&update)
incid, _, err := findIncident(r)
if err != nil {
sendErrorJson(err, w, r)
return
}
update.IncidentId = utils.ToInt(vars["id"])
var update *incidents.IncidentUpdate
if err := DecodeJSON(r, &update); err != nil {
sendErrorJson(err, w, r)
return
}
update.IncidentId = incid.Id
err = update.Create()
if err != nil {
@ -45,13 +63,17 @@ func apiCreateIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
}
func apiCreateIncidentHandler(w http.ResponseWriter, r *http.Request) {
var incident *incidents.Incident
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&incident)
service, err := findService(r)
if err != nil {
sendErrorJson(err, w, r)
return
}
var incident *incidents.Incident
if err := DecodeJSON(r, &incident); err != nil {
sendErrorJson(err, w, r)
return
}
incident.ServiceId = service.Id
err = incident.Create()
if err != nil {
sendErrorJson(err, w, r)
@ -61,16 +83,12 @@ func apiCreateIncidentHandler(w http.ResponseWriter, r *http.Request) {
}
func apiIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incident, err := incidents.Find(utils.ToInt(vars["id"]))
incident, _, err := findIncident(r)
if err != nil {
sendErrorJson(err, w, r)
return
}
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&incident)
if err != nil {
if err := DecodeJSON(r, &incident); err != nil {
sendErrorJson(err, w, r)
return
}
@ -80,8 +98,7 @@ func apiIncidentUpdateHandler(w http.ResponseWriter, r *http.Request) {
}
func apiDeleteIncidentHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
incident, err := incidents.Find(utils.ToInt(vars["id"]))
incident, _, err := findIncident(r)
if err != nil {
sendErrorJson(err, w, r)
return

155
handlers/incidents_test.go Normal file
View File

@ -0,0 +1,155 @@
package handlers
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestUnAuthenticatedIncidentRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "No Authentication - New Incident",
URL: "/api/services/1/incidents",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - New Incident Update",
URL: "/api/incidents/updates",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Update Incident",
URL: "/api/incidents/1",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Delete Incident",
URL: "/api/incidents/1",
Method: "DELETE",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Delete Incident Update",
URL: "/api/incidents/1/updates/1",
Method: "DELETE",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
str, t, err := RunHTTPTest(v, t)
t.Logf("Test %s: \n %v\n", v.Name, str)
assert.Nil(t, err)
})
}
}
func TestIncidentsAPIRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "Statping Create Incident",
URL: "/api/services/1/incidents",
Method: "POST",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
AfterTest: UnsetTestENV,
Body: `{
"title": "New Incident",
"description": "This is a test for incidents"
}`,
ExpectedContains: []string{Success},
},
{
Name: "Statping Service 1 Incidents",
URL: "/api/services/1/incidents",
Method: "GET",
ExpectedStatus: 200,
ResponseLen: 1,
BeforeTest: SetTestENV,
AfterTest: UnsetTestENV,
ExpectedContains: []string{`"title":"New Incident"`},
},
{
Name: "Statping Update Incident",
URL: "/api/incidents/1",
Body: `{
"title": "Updated Incident",
"description": "This is an updated incidents"
}`,
Method: "POST",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
ExpectedContains: []string{Success},
},
{
Name: "Statping View Incident Updates",
URL: "/api/incidents/1/updates",
Method: "GET",
ExpectedStatus: 200,
ResponseLen: 3,
BeforeTest: SetTestENV,
ExpectedContains: []string{`"type":"investigating"`},
},
{
Name: "Statping Create Incident Update",
URL: "/api/incidents/1/updates",
Method: "POST",
Body: `{
"message": "Test message here",
"type": "Update"
}`,
ExpectedStatus: 200,
BeforeTest: SetTestENV,
ExpectedContains: []string{Success},
},
{
Name: "Incorrect Checkin JSON POST",
URL: "/api/incidents/1/updates",
Body: BadJSON,
ExpectedContains: []string{BadJSONResponse},
BeforeTest: SetTestENV,
Method: "POST",
ExpectedStatus: 422,
},
{
Name: "Statping Delete Incident Update",
URL: "/api/incidents/1/updates/1",
Method: "DELETE",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
ExpectedContains: []string{Success},
},
{
Name: "Statping Delete Incident",
URL: "/api/incidents/1",
Method: "DELETE",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
ExpectedContains: []string{Success},
},
{
Name: "Incorrect JSON POST",
URL: "/api/services/1/incidents",
Body: BadJSON,
ExpectedContains: []string{BadJSONResponse},
Method: "POST",
ExpectedStatus: 422,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
_, t, err := RunHTTPTest(v, t)
assert.Nil(t, err)
})
}
}

View File

@ -8,7 +8,7 @@ import (
func indexHandler(w http.ResponseWriter, r *http.Request) {
if !core.App.Setup {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
ExecuteResponse(w, r, "base.gohtml", core.App, "setup")
return
}
ExecuteResponse(w, r, "base.gohtml", core.App, nil)

View File

@ -1,21 +1,24 @@
package handlers
import (
"fmt"
"github.com/gorilla/mux"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/messages"
"github.com/statping/statping/utils"
"net/http"
)
func getMessageByID(r *http.Request) (*messages.Message, int64, error) {
func findMessage(r *http.Request) (*messages.Message, int64, error) {
vars := mux.Vars(r)
num := utils.ToInt(vars["id"])
message, err := messages.Find(num)
if err != nil {
return nil, num, err
if utils.NotNumber(vars["id"]) {
return nil, 0, errors.NotNumber
}
return message, num, nil
id := utils.ToInt(vars["id"])
message, err := messages.Find(id)
if err != nil {
return nil, id, err
}
return message, id, nil
}
func apiAllMessagesHandler(r *http.Request) interface{} {
@ -37,17 +40,17 @@ func apiMessageCreateHandler(w http.ResponseWriter, r *http.Request) {
}
func apiMessageGetHandler(r *http.Request) interface{} {
message, id, err := getMessageByID(r)
message, _, err := findMessage(r)
if err != nil {
return fmt.Errorf("message #%d was not found", id)
return err
}
return message
}
func apiMessageDeleteHandler(w http.ResponseWriter, r *http.Request) {
message, id, err := getMessageByID(r)
message, _, err := findMessage(r)
if err != nil {
sendErrorJson(fmt.Errorf("message #%d was not found", id), w, r)
sendErrorJson(err, w, r)
return
}
err = message.Delete()
@ -59,9 +62,9 @@ func apiMessageDeleteHandler(w http.ResponseWriter, r *http.Request) {
}
func apiMessageUpdateHandler(w http.ResponseWriter, r *http.Request) {
message, id, err := getMessageByID(r)
message, _, err := findMessage(r)
if err != nil {
sendErrorJson(fmt.Errorf("message #%d was not found", id), w, r)
sendErrorJson(err, w, r)
return
}
if err := DecodeJSON(r, &message); err != nil {

View File

@ -5,6 +5,40 @@ import (
"testing"
)
func TestUnAuthenticatedMessageRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "No Authentication - New Message",
URL: "/api/messages",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Update Message",
URL: "/api/messages/1",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Delete Message",
URL: "/api/messages/1",
Method: "DELETE",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
str, t, err := RunHTTPTest(v, t)
t.Logf("Test %s: \n %v\n", v.Name, str)
assert.Nil(t, err)
})
}
}
func TestMessagesApiRoutes(t *testing.T) {
tests := []HTTPTest{
{
@ -29,7 +63,7 @@ func TestMessagesApiRoutes(t *testing.T) {
"notify_before_scale": "hour"
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success"`, `"type":"message"`, `"method":"create"`, `"title":"API Message"`},
ExpectedContains: []string{Success, `"type":"message"`, `"method":"create"`, `"title":"API Message"`},
BeforeTest: SetTestENV,
AfterTest: UnsetTestENV,
SecureRoute: true,
@ -40,7 +74,8 @@ func TestMessagesApiRoutes(t *testing.T) {
Method: "GET",
ExpectedStatus: 200,
ExpectedContains: []string{`"title":"Routine Downtime"`},
}, {
},
{
Name: "Statping Update Message",
URL: "/api/messages/1",
Method: "POST",
@ -56,7 +91,7 @@ func TestMessagesApiRoutes(t *testing.T) {
"notify_before_scale": "hour"
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success"`, `"type":"message"`, `"method":"update"`},
ExpectedContains: []string{Success, `"type":"message"`, MethodUpdate},
BeforeTest: SetTestENV,
SecureRoute: true,
},
@ -65,10 +100,26 @@ func TestMessagesApiRoutes(t *testing.T) {
URL: "/api/messages/1",
Method: "DELETE",
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success"`, `"method":"delete"`},
ExpectedContains: []string{Success, MethodDelete},
BeforeTest: SetTestENV,
SecureRoute: true,
}}
},
{
Name: "Statping Missing Message",
URL: "/api/messages/999999",
Method: "GET",
ExpectedStatus: 404,
},
{
Name: "Incorrect JSON POST",
URL: "/api/messages",
Body: BadJSON,
ExpectedContains: []string{BadJSONResponse},
BeforeTest: SetTestENV,
Method: "POST",
ExpectedStatus: 422,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {

View File

@ -4,9 +4,9 @@ import (
"compress/gzip"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/utils"
"io"
"net/http"
@ -162,3 +162,13 @@ func cached(duration, contentType string, handler func(w http.ResponseWriter, r
}
})
}
func DecodeJSON(r *http.Request, obj interface{}) error {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&obj)
if err != nil {
return errors.DecodeJSON
}
defer r.Body.Close()
return nil
}

View File

@ -1,7 +1,6 @@
package handlers
import (
"encoding/json"
"github.com/gorilla/mux"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/services"
@ -41,12 +40,11 @@ func apiNotifierUpdateHandler(w http.ResponseWriter, r *http.Request) {
return
}
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&notifer)
if err != nil {
if err := DecodeJSON(r, &notifer); err != nil {
sendErrorJson(err, w, r)
return
}
err = notifer.Update()
if err != nil {
sendErrorJson(err, w, r)
@ -64,9 +62,7 @@ func testNotificationHandler(w http.ResponseWriter, r *http.Request) {
return
}
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&notifer)
if err != nil {
if err := DecodeJSON(r, &notifer); err != nil {
sendErrorJson(err, w, r)
return
}

View File

@ -11,6 +11,40 @@ func TestAttachment(t *testing.T) {
notifiers.InitNotifiers()
}
func TestUnAuthenticatedNotifierRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "No Authentication - View All Notifiers",
URL: "/api/notifiers",
Method: "GET",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - View Notifier",
URL: "/api/notifier/slack",
Method: "GET",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Update Notifier",
URL: "/api/notifier/slack",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
str, t, err := RunHTTPTest(v, t)
t.Logf("Test %s: \n %v\n", v.Name, str)
assert.Nil(t, err)
})
}
}
func TestApiNotifiersRoutes(t *testing.T) {
tests := []HTTPTest{
{
@ -49,7 +83,17 @@ func TestApiNotifiersRoutes(t *testing.T) {
ExpectedContains: []string{`"method":"slack"`},
BeforeTest: SetTestENV,
SecureRoute: true,
}}
},
{
Name: "Incorrect JSON POST",
URL: "/api/notifier/slack",
Body: BadJSON,
ExpectedContains: []string{BadJSONResponse},
BeforeTest: SetTestENV,
Method: "POST",
ExpectedStatus: 422,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {

View File

@ -34,10 +34,13 @@ func oauthHandler(w http.ResponseWriter, r *http.Request) {
err, oauth = googleOAuth(r)
case "github":
err, oauth = githubOAuth(r)
case "slack":
err, oauth = slackOAuth(r)
}
if err != nil {
log.Error(err)
sendErrorJson(err, w, r)
return
}
@ -45,6 +48,7 @@ func oauthHandler(w http.ResponseWriter, r *http.Request) {
}
func oauthLogin(oauth *oAuth, w http.ResponseWriter, r *http.Request) {
log.Infoln(oauth)
user := &users.User{
Id: 0,
Username: oauth.Email,
@ -80,7 +84,7 @@ func githubOAuth(r *http.Request) (error, *oAuth) {
}
func googleOAuth(r *http.Request) (error, *oAuth) {
c := *core.App
c := core.App
code := r.URL.Query().Get("code")
config := &oauth2.Config{

View File

@ -40,7 +40,7 @@ func hex2int(hexStr string) uint64 {
func prometheusHandler(w http.ResponseWriter, r *http.Request) {
promValues = []string{}
prefix = utils.Getenv("PREFIX", "").(string)
prefix = utils.Params.GetString("PREFIX")
if prefix != "" {
prefix = prefix + "_"
}
@ -60,10 +60,6 @@ func prometheusHandler(w http.ResponseWriter, r *http.Request) {
PrometheusKeyValue("total_services", len(services.Services()))
PrometheusKeyValue("seconds_online", secondsOnline)
if secondsOnline < 5 {
return
}
for _, ser := range services.AllInOrder() {
online := 1
if !ser.Online {

View File

@ -1,26 +0,0 @@
package handlers
import (
"encoding/json"
"errors"
"github.com/gorilla/mux"
"github.com/statping/statping/utils"
"net/http"
)
func DecodeJSON(r *http.Request, obj interface{}) error {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&obj)
if err != nil {
return err
}
return nil
}
func GetID(r *http.Request) (int64, error) {
vars := mux.Vars(r)
if vars["id"] == "" {
return 0, errors.New("no id specified in request")
}
return utils.ToInt(vars["id"]), nil
}

View File

@ -26,14 +26,14 @@ func Router() *mux.Router {
CacheStorage = NewStorage()
r := mux.NewRouter().StrictSlash(true)
authUser := utils.Getenv("AUTH_USERNAME", "").(string)
authPass := utils.Getenv("AUTH_PASSWORD", "").(string)
authUser := utils.Params.GetString("AUTH_USERNAME")
authPass := utils.Params.GetString("AUTH_PASSWORD")
if authUser != "" && authPass != "" {
r.Use(basicAuthHandler)
}
bPath := utils.Getenv("BASE_PATH", "").(string)
bPath := utils.Params.GetString("BASE_PATH")
sentryHandler := sentryhttp.New(sentryhttp.Options{})
if bPath != "" {
@ -78,6 +78,7 @@ func Router() *mux.Router {
api.Handle("/api/cache", authenticated(apiCacheHandler, false)).Methods("GET")
api.Handle("/api/clear_cache", authenticated(apiClearCacheHandler, false))
api.Handle("/api/core", authenticated(apiCoreHandler, false)).Methods("POST")
api.Handle("/api/oauth", scoped(apiOAuthHandler)).Methods("GET")
api.Handle("/api/logs", authenticated(logsHandler, false)).Methods("GET")
api.Handle("/api/logs/last", authenticated(logsLineHandler, false)).Methods("GET")
@ -100,7 +101,6 @@ func Router() *mux.Router {
api.Handle("/api/services", authenticated(apiCreateServiceHandler, false)).Methods("POST")
api.Handle("/api/services/{id}", scoped(apiServiceHandler)).Methods("GET")
api.Handle("/api/reorder/services", authenticated(reorderServiceHandler, false)).Methods("POST")
api.Handle("/api/services/{id}/running", authenticated(apiServiceRunningHandler, false)).Methods("POST")
api.Handle("/api/services/{id}", authenticated(apiServiceUpdateHandler, false)).Methods("POST")
api.Handle("/api/services/{id}", authenticated(apiServiceDeleteHandler, false)).Methods("DELETE")
api.Handle("/api/services/{id}/failures", scoped(apiServiceFailuresHandler)).Methods("GET")
@ -112,7 +112,6 @@ func Router() *mux.Router {
api.Handle("/api/services/{id}/failure_data", cached("30s", "application/json", apiServiceFailureDataHandler)).Methods("GET")
api.Handle("/api/services/{id}/ping_data", cached("30s", "application/json", apiServicePingDataHandler)).Methods("GET")
api.Handle("/api/services/{id}/uptime_data", http.HandlerFunc(apiServiceTimeDataHandler)).Methods("GET")
//api.Handle("/api/services/{id}/heatmap", cached("30s", "application/json", apiServiceHeatmapHandler)).Methods("GET")
// API INCIDENTS Routes
api.Handle("/api/services/{id}/incidents", http.HandlerFunc(apiServiceIncidentsHandler)).Methods("GET")
@ -152,17 +151,11 @@ func Router() *mux.Router {
api.Handle("/api/checkins/{api}", authenticated(checkinDeleteHandler, false)).Methods("DELETE")
r.Handle("/checkin/{api}", http.HandlerFunc(checkinHitHandler))
// Static Files Routes
r.PathPrefix("/files/postman.json").Handler(http.StripPrefix("/files/", http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/files/swagger.json").Handler(http.StripPrefix("/files/", http.FileServer(source.TmplBox.HTTPBox())))
r.PathPrefix("/files/grafana.json").Handler(http.StripPrefix("/files/", http.FileServer(source.TmplBox.HTTPBox())))
// API Generic Routes
r.Handle("/metrics", readOnly(prometheusHandler, false))
r.Handle("/health", http.HandlerFunc(healthCheckHandler))
r.Handle("/oauth/{provider}", http.HandlerFunc(oauthHandler))
api.Handle("/api/oauth/{provider}", http.HandlerFunc(oauthHandler))
r.Handle("/.well-known/", http.StripPrefix("/.well-known/", http.FileServer(http.Dir(dir+"/.well-known"))))
r.NotFoundHandler = http.HandlerFunc(error404Handler)
return r
}

View File

@ -2,8 +2,8 @@ package handlers
import (
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/statping/statping/database"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/hits"
"github.com/statping/statping/types/services"
@ -16,12 +16,16 @@ type serviceOrder struct {
Order int `json:"order"`
}
func serviceByID(r *http.Request) (*services.Service, error) {
func findService(r *http.Request) (*services.Service, error) {
vars := mux.Vars(r)
id := utils.ToInt(vars["id"])
servicer, err := services.Find(id)
if err != nil {
return nil, errors.Errorf("service %d not found", id)
return nil, err
}
user := IsUser(r)
if !servicer.Public.Bool && !user {
return nil, errors.NotAuthenticated
}
return servicer, nil
}
@ -37,7 +41,7 @@ func reorderServiceHandler(w http.ResponseWriter, r *http.Request) {
for _, s := range newOrder {
service, err := services.Find(s.Id)
if err != nil {
sendErrorJson(errors.Errorf("service %d not found", s.Id), w, r)
sendErrorJson(err, w, r)
return
}
service.Order = s.Order
@ -47,14 +51,10 @@ func reorderServiceHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceHandler(r *http.Request) interface{} {
srv, err := serviceByID(r)
srv, err := findService(r)
if err != nil {
return err
}
user := IsUser(r)
if !srv.Public.Bool && !user {
return errors.New("not authenticated")
}
srv = srv.UpdateStats()
return *srv
}
@ -76,13 +76,13 @@ func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r)
service, err := findService(r)
if err != nil {
sendErrorJson(err, w, r, http.StatusNotFound)
sendErrorJson(err, w, r)
return
}
if err := DecodeJSON(r, &service); err != nil {
sendErrorJson(err, w, r, http.StatusBadRequest)
sendErrorJson(err, w, r)
return
}
@ -95,27 +95,12 @@ func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
sendJsonAction(service, "update", w, r)
}
func apiServiceRunningHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r)
func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
service, err := findService(r)
if err != nil {
sendErrorJson(err, w, r)
return
}
if service.IsRunning() {
service.Close()
} else {
service.Start()
}
sendJsonAction(service, "running", w, r)
}
func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
service, err := services.Find(utils.ToInt(vars["id"]))
if err != nil {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupQuery, err := database.ParseQueries(r, service.AllHits())
if err != nil {
@ -132,10 +117,9 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
service, err := services.Find(utils.ToInt(vars["id"]))
service, err := findService(r)
if err != nil {
sendErrorJson(errors.New("service data not found"), w, r)
sendErrorJson(err, w, r)
return
}
@ -155,9 +139,9 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r)
service, err := findService(r)
if err != nil {
sendErrorJson(errors.New("service data not found"), w, r)
sendErrorJson(err, w, r)
return
}
@ -177,9 +161,9 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceTimeDataHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r)
service, err := findService(r)
if err != nil {
sendErrorJson(errors.New("service data not found"), w, r)
sendErrorJson(err, w, r)
return
}
@ -217,7 +201,7 @@ func apiServiceTimeDataHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceDeleteHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r)
service, err := findService(r)
if err != nil {
sendErrorJson(err, w, r)
return
@ -244,7 +228,7 @@ func apiAllServicesHandler(r *http.Request) interface{} {
}
func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r)
service, err := findService(r)
if err != nil {
sendErrorJson(err, w, r)
return
@ -258,12 +242,10 @@ func servicesDeleteFailuresHandler(w http.ResponseWriter, r *http.Request) {
}
func apiServiceFailuresHandler(r *http.Request) interface{} {
vars := mux.Vars(r)
service, err := services.Find(utils.ToInt(vars["id"]))
service, err := findService(r)
if err != nil {
return errors.New("service not found")
return err
}
var fails []*failures.Failure
query, err := database.ParseQueries(r, service.AllFailures())
if err != nil {
@ -274,12 +256,10 @@ func apiServiceFailuresHandler(r *http.Request) interface{} {
}
func apiServiceHitsHandler(r *http.Request) interface{} {
vars := mux.Vars(r)
service, err := services.Find(utils.ToInt(vars["id"]))
service, err := findService(r)
if err != nil {
return errors.New("service not found")
return err
}
var hts []*hits.Hit
query, err := database.ParseQueries(r, service.AllHits())
if err != nil {

View File

@ -1,6 +1,7 @@
package handlers
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/statping/statping/types"
@ -9,12 +10,47 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestApiServiceRoutes(t *testing.T) {
func TestUnAuthenticatedServicesRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "No Authentication - New Service",
URL: "/api/services",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Update Service",
URL: "/api/services/1",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Delete Service",
URL: "/api/services/1",
Method: "DELETE",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
str, t, err := RunHTTPTest(v, t)
t.Logf("Test %s: \n %v\n", v.Name, str)
assert.Nil(t, err)
})
}
}
func TestApiServiceRoutes(t *testing.T) {
since := utils.Now().Add(-30 * types.Day)
startEndQuery := fmt.Sprintf("?start=%d&end=%d", since.Unix(), utils.Now().Unix())
end := utils.Now().Add(-30 * time.Minute)
startEndQuery := fmt.Sprintf("?start=%d&end=%d", since.Unix(), end.Unix()+15)
tests := []HTTPTest{
{
@ -58,13 +94,20 @@ func TestApiServiceRoutes(t *testing.T) {
BeforeTest: UnsetTestENV,
},
{
Name: "Statping Private Service 1",
Name: "Statping Private Service 6",
URL: "/api/services/6",
Method: "GET",
ExpectedContains: []string{`"error":"not authenticated"`},
ExpectedStatus: 200,
ExpectedContains: []string{`"error":"user not authenticated"`},
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "Statping Authenticated Private Service 6",
URL: "/api/services/6",
Method: "GET",
ExpectedStatus: 200,
BeforeTest: SetTestENV,
},
{
Name: "Statping Service 1 with Private responses",
URL: "/api/services/1",
@ -75,9 +118,23 @@ func TestApiServiceRoutes(t *testing.T) {
},
{
Name: "Statping Service Failures",
URL: "/api/services/1/failures",
URL: "/api/services/1/failures" + startEndQuery,
Method: "GET",
ResponseLen: 125,
GreaterThan: 120,
ExpectedStatus: 200,
},
{
Name: "Statping Service 1 Hits",
URL: "/api/services/1/hits" + startEndQuery,
Method: "GET",
GreaterThan: 8580,
ExpectedStatus: 200,
},
{
Name: "Statping Service 2 Hits",
URL: "/api/services/2/hits" + startEndQuery,
Method: "GET",
GreaterThan: 8580,
ExpectedStatus: 200,
},
{
@ -88,55 +145,73 @@ func TestApiServiceRoutes(t *testing.T) {
ExpectedStatus: 200,
},
{
Name: "Statping Service 1 Data",
Name: "Statping Service 1 Hits Data",
URL: "/api/services/1/hits_data" + startEndQuery,
Method: "GET",
ResponseLen: 73,
GreaterThan: 70,
ExpectedStatus: 200,
},
{
Name: "Statping Service 1 Ping Data",
URL: "/api/services/1/ping_data" + startEndQuery,
Method: "GET",
ResponseLen: 73,
ExpectedStatus: 200,
GreaterThan: 70,
},
{
Name: "Statping Service 1 Failure Data - 24 Hour",
URL: "/api/services/1/failure_data" + startEndQuery + "&group=24h",
Method: "GET",
ExpectedStatus: 200,
GreaterThan: 3,
},
{
Name: "Statping Service 1 Failure Data - 12 Hour",
URL: "/api/services/1/failure_data" + startEndQuery + "&group=12h",
Method: "GET",
ExpectedStatus: 200,
GreaterThan: 6,
},
{
Name: "Statping Service 1 Failure Data - 1 Hour",
URL: "/api/services/1/failure_data" + startEndQuery + "&group=1h",
Method: "GET",
ExpectedStatus: 200,
GreaterThan: 70,
},
{
Name: "Statping Service 1 Failure Data - 15 Minute",
URL: "/api/services/1/failure_data" + startEndQuery + "&group=15m",
Method: "GET",
ResponseLen: 124,
GreaterThan: 120,
ExpectedStatus: 200,
},
{
Name: "Statping Service 1 Hits",
URL: "/api/services/1/hits_data" + startEndQuery,
Method: "GET",
ResponseLen: 73,
GreaterThan: 70,
ExpectedStatus: 200,
},
{
Name: "Statping Service 1 Uptime",
URL: "/api/services/1/uptime_data" + startEndQuery,
Method: "GET",
ExpectedStatus: 200,
ResponseFunc: func(t *testing.T, resp []byte) error {
var uptime *services.UptimeSeries
if err := json.Unmarshal(resp, &uptime); err != nil {
return err
}
assert.GreaterOrEqual(t, uptime.Uptime, int64(200000000))
return nil
},
},
{
Name: "Statping Service 1 Failure Data",
URL: "/api/services/1/failure_data" + startEndQuery,
Method: "GET",
GreaterThan: 70,
ExpectedStatus: 200,
},
{
@ -169,7 +244,7 @@ func TestApiServiceRoutes(t *testing.T) {
"order_id": 0
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success","type":"service","method":"create"`, `"public":false`, `"group_id":1`},
ExpectedContains: []string{Success, `"type":"service","method":"create"`, `"public":false`, `"group_id":1`},
FuncTest: func(t *testing.T) error {
count := len(services.Services())
if count != 7 {
@ -198,7 +273,7 @@ func TestApiServiceRoutes(t *testing.T) {
"order_id": 0
}`,
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success"`, `"name":"Updated New Service"`, `"method":"update"`},
ExpectedContains: []string{Success, `"name":"Updated New Service"`, MethodUpdate},
FuncTest: func(t *testing.T) error {
item, err := services.Find(1)
require.Nil(t, err)
@ -214,7 +289,7 @@ func TestApiServiceRoutes(t *testing.T) {
URL: "/api/services/1/failures",
Method: "DELETE",
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success"`, `"method":"delete_failures"`},
ExpectedContains: []string{Success, `"method":"delete_failures"`},
FuncTest: func(t *testing.T) error {
item, err := services.Find(1)
require.Nil(t, err)
@ -231,7 +306,7 @@ func TestApiServiceRoutes(t *testing.T) {
URL: "/api/services/1",
Method: "DELETE",
ExpectedStatus: 200,
ExpectedContains: []string{`"status":"success"`, `"method":"delete"`},
ExpectedContains: []string{Success, MethodDelete},
FuncTest: func(t *testing.T) error {
count := len(services.Services())
if count != 6 {
@ -240,7 +315,17 @@ func TestApiServiceRoutes(t *testing.T) {
return nil
},
SecureRoute: true,
}}
},
{
Name: "Incorrect JSON POST",
URL: "/api/services",
Body: BadJSON,
ExpectedContains: []string{BadJSONResponse},
BeforeTest: SetTestENV,
Method: "POST",
ExpectedStatus: 422,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {

View File

@ -32,13 +32,8 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
log.WithFields(utils.ToFields(core.App, confgs)).Debugln("new configs posted")
if err = configs.ConnectConfigs(confgs); err != nil {
if err = configs.ConnectConfigs(confgs, false); err != nil {
log.Errorln(err)
if err := confgs.Delete(); err != nil {
log.Errorln(err)
sendErrorJson(err, w, r)
return
}
sendErrorJson(err, w, r)
return
}
@ -83,14 +78,14 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
c := &core.Core{
Name: project,
Description: description,
//ApiKey: apiKey.(string),
//ApiSecret: apiSecret.(string),
Domain: domain,
Version: core.App.Version,
Started: utils.Now(),
CreatedAt: utils.Now(),
UseCdn: null.NewNullBool(false),
Footer: null.NewNullString(""),
ApiKey: utils.Params.GetString("API_KEY"),
ApiSecret: utils.Params.GetString("API_SECRET"),
Domain: domain,
Version: core.App.Version,
Started: utils.Now(),
CreatedAt: utils.Now(),
UseCdn: null.NewNullBool(false),
Footer: null.NewNullString(""),
}
log.Infoln("Creating new Core")

View File

@ -7,7 +7,50 @@ import (
"testing"
)
func TestUnAuthenticatedThemeRoutes(t *testing.T) {
t.SkipNow()
tests := []HTTPTest{
{
Name: "No Authentication - Create Themes",
URL: "/api/theme/create",
Method: "GET",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - View Themes",
URL: "/api/theme",
Method: "GET",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Update Themes",
URL: "/api/theme",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Delete Themes",
URL: "/api/theme",
Method: "DELETE",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
str, t, err := RunHTTPTest(v, t)
t.Logf("Test %s: \n %v\n", v.Name, str)
assert.Nil(t, err)
})
}
}
func TestThemeRoutes(t *testing.T) {
t.SkipNow()
tests := []HTTPTest{
{
Name: "Create Theme Assets",
@ -17,7 +60,7 @@ func TestThemeRoutes(t *testing.T) {
ExpectedContains: []string{`"status":"success"`},
BeforeTest: SetTestENV,
AfterTest: func(t *testing.T) error {
assert.True(t, source.UsingAssets(utils.Directory))
assert.True(t, source.UsingAssets(utils.Params.GetString("STATPING_DIR")))
return nil
},
},

View File

@ -1,28 +1,31 @@
package handlers
import (
"errors"
"fmt"
"github.com/gorilla/mux"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/types/users"
"github.com/statping/statping/utils"
"net/http"
)
func getUser(r *http.Request) (*users.User, int64, error) {
func findUser(r *http.Request) (*users.User, int64, error) {
vars := mux.Vars(r)
if utils.NotNumber(vars["id"]) {
return nil, 0, errors.NotNumber
}
num := utils.ToInt(vars["id"])
user, err := users.Find(num)
if err != nil {
return nil, num, err
return nil, num, errors.Missing(&users.User{}, num)
}
return user, num, nil
}
func apiUserHandler(w http.ResponseWriter, r *http.Request) {
user, _, err := getUser(r)
user, _, err := findUser(r)
if err != nil {
sendErrorJson(err, w, r, http.StatusNotFound)
sendErrorJson(err, w, r)
return
}
user.Password = ""
@ -30,15 +33,15 @@ func apiUserHandler(w http.ResponseWriter, r *http.Request) {
}
func apiUserUpdateHandler(w http.ResponseWriter, r *http.Request) {
user, id, err := getUser(r)
user, _, err := findUser(r)
if err != nil {
sendErrorJson(fmt.Errorf("user #%d was not found", id), w, r)
sendErrorJson(err, w, r)
return
}
err = DecodeJSON(r, &user)
if err != nil {
sendErrorJson(fmt.Errorf("user #%d was not found", id), w, r)
sendErrorJson(err, w, r)
return
}
@ -60,7 +63,7 @@ func apiUserDeleteHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(errors.New("cannot delete the last user"), w, r)
return
}
user, _, err := getUser(r)
user, _, err := findUser(r)
if err != nil {
sendErrorJson(err, w, r)
return

View File

@ -1,11 +1,54 @@
package handlers
import (
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/url"
"testing"
)
func TestUnAuthenticatedUserRoutes(t *testing.T) {
tests := []HTTPTest{
{
Name: "No Authentication - New User",
URL: "/api/users",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Update User",
URL: "/api/users/1",
Method: "POST",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - View User",
URL: "/api/users/1",
Method: "GET",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
{
Name: "No Authentication - Delete User",
URL: "/api/users/1",
Method: "DELETE",
ExpectedStatus: 401,
BeforeTest: UnsetTestENV,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {
str, t, err := RunHTTPTest(v, t)
t.Logf("Test %s: \n %v\n", v.Name, str)
assert.Nil(t, err)
})
}
}
func TestApiUsersRoutes(t *testing.T) {
form := url.Values{}
form.Add("username", "adminupdated")
@ -16,12 +59,29 @@ func TestApiUsersRoutes(t *testing.T) {
badForm.Add("password", "wrongpassword")
tests := []HTTPTest{
{
Name: "Check Basic Authentication",
URL: "/api",
Method: "GET",
ExpectedStatus: 401,
BeforeTest: func(t *testing.T) error {
utils.Params.Set("AUTH_USERNAME", "admin")
utils.Params.Set("AUTH_PASSWORD", "admin")
return nil
},
AfterTest: func(t *testing.T) error {
utils.Params.Set("AUTH_USERNAME", "")
utils.Params.Set("AUTH_PASSWORD", "")
return nil
},
},
{
Name: "Statping All Users",
URL: "/api/users",
Method: "GET",
ExpectedStatus: 200,
ResponseLen: 1,
BeforeTest: SetTestENV,
}, {
Name: "Statping Create User",
URL: "/api/users",
@ -33,12 +93,18 @@ func TestApiUsersRoutes(t *testing.T) {
"password": "passsword123",
"admin": true
}`,
ExpectedStatus: 200,
ExpectedStatus: 200,
ExpectedContains: []string{Success, MethodCreate},
}, {
Name: "Statping View User",
URL: "/api/users/1",
Method: "GET",
ExpectedStatus: 200,
}, {
Name: "Statping Incorrect User ID",
URL: "/api/users/NOinteger",
Method: "GET",
ExpectedStatus: 422,
}, {
Name: "Statping Missing User",
URL: "/api/users/9393939393",
@ -54,12 +120,14 @@ func TestApiUsersRoutes(t *testing.T) {
"password": "password12345",
"admin": true
}`,
ExpectedStatus: 200,
ExpectedStatus: 200,
ExpectedContains: []string{Success, MethodUpdate},
}, {
Name: "Statping Delete User",
URL: "/api/users/2",
Method: "DELETE",
ExpectedStatus: 200,
Name: "Statping Delete User",
URL: "/api/users/2",
Method: "DELETE",
ExpectedStatus: 200,
ExpectedContains: []string{Success, MethodDelete},
}, {
Name: "Statping Login User",
URL: "/api/login",
@ -81,7 +149,17 @@ func TestApiUsersRoutes(t *testing.T) {
URL: "/api/logout",
Method: "GET",
ExpectedStatus: 303,
}}
},
{
Name: "Incorrect JSON POST",
URL: "/api/users",
Body: BadJSON,
ExpectedContains: []string{BadJSONResponse},
BeforeTest: SetTestENV,
Method: "POST",
ExpectedStatus: 422,
},
}
for _, v := range tests {
t.Run(v.Name, func(t *testing.T) {

View File

@ -4,6 +4,7 @@ import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/null"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
@ -12,7 +13,7 @@ import (
)
var (
DISCORD_URL = os.Getenv("DISCORD_URL")
DISCORD_URL = utils.Params.GetString("DISCORD_URL")
discordMessage = `{"content": "The discord notifier on Statping has been tested!"}`
)

View File

@ -8,7 +8,6 @@ import (
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"testing"
"time"
)
@ -25,12 +24,12 @@ var (
var testEmail *emailOutgoing
func init() {
EMAIL_HOST = os.Getenv("EMAIL_HOST")
EMAIL_USER = os.Getenv("EMAIL_USER")
EMAIL_PASS = os.Getenv("EMAIL_PASS")
EMAIL_OUTGOING = os.Getenv("EMAIL_OUTGOING")
EMAIL_SEND_TO = os.Getenv("EMAIL_SEND_TO")
EMAIL_PORT = utils.ToInt(os.Getenv("EMAIL_PORT"))
EMAIL_HOST = utils.Params.GetString("EMAIL_HOST")
EMAIL_USER = utils.Params.GetString("EMAIL_USER")
EMAIL_PASS = utils.Params.GetString("EMAIL_PASS")
EMAIL_OUTGOING = utils.Params.GetString("EMAIL_OUTGOING")
EMAIL_SEND_TO = utils.Params.GetString("EMAIL_SEND_TO")
EMAIL_PORT = utils.ToInt(utils.Params.GetString("EMAIL_PORT"))
}
func TestEmailNotifier(t *testing.T) {

View File

@ -4,9 +4,9 @@ import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/null"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
"testing"
"time"
)
@ -21,7 +21,7 @@ func TestSlackNotifier(t *testing.T) {
db.AutoMigrate(&notifications.Notification{})
notifications.SetDB(db)
SLACK_URL = os.Getenv("SLACK_URL")
SLACK_URL = utils.Params.GetString("SLACK_URL")
slacker.Host = SLACK_URL
slacker.Enabled = null.NewNullBool(true)

View File

@ -9,6 +9,7 @@ import (
"github.com/russross/blackfriday/v2"
"github.com/statping/statping/utils"
"os"
"os/exec"
"path/filepath"
"strings"
)
@ -42,10 +43,13 @@ func scssRendered(name string) string {
// CompileSASS will attempt to compile the SASS files into CSS
func CompileSASS(files ...string) error {
sassBin := utils.Getenv("SASS", "sass").(string)
sassBin, err := exec.LookPath("sass")
if err != nil {
return err
}
for _, file := range files {
scssFile := fmt.Sprintf("%v/assets/%v", utils.Directory, file)
scssFile := fmt.Sprintf("%v/assets/%v", utils.Params.GetString("STATPING_DIR"), file)
log.Infoln(fmt.Sprintf("Compiling SASS %v into %v", scssFile, scssRendered(scssFile)))
@ -57,10 +61,10 @@ func CompileSASS(files ...string) error {
return errors.Wrapf(err, "failed to compile assets, %s %s %s", err, stdout, stderr)
}
if stdout != "" || stderr != "" {
log.Errorln(fmt.Sprintf("Failed to compile assets with SASS %v %v %v", err, stdout, stderr))
return errors.Wrap(err, "failed to capture stdout or stderr")
}
//if stdout != "" || stderr != "" {
// log.Errorln(fmt.Sprintf("Failed to compile assets with SASS %v %v %v", err, stdout, stderr))
// return errors.Wrap(err, "failed to capture stdout or stderr")
//}
if stdout != "" || stderr != "" {
log.Infoln(fmt.Sprintf("out: %v | error: %v", stdout, stderr))
@ -75,7 +79,7 @@ func UsingAssets(folder string) bool {
if _, err := os.Stat(folder + "/assets"); err == nil {
return true
} else {
useAssets := utils.Getenv("USE_ASSETS", false).(bool)
useAssets := utils.Params.GetBool("USE_ASSETS")
if useAssets {
log.Infoln("Environment variable USE_ASSETS was found.")
@ -137,9 +141,6 @@ func CreateAllAssets(folder string) error {
CopyToPublic(TmplBox, "", "robots.txt")
CopyToPublic(TmplBox, "", "banner.png")
CopyToPublic(TmplBox, "", "favicon.ico")
CopyToPublic(TmplBox, "files", "swagger.json")
CopyToPublic(TmplBox, "files", "postman.json")
CopyToPublic(TmplBox, "files", "grafana.json")
log.Infoln("Compiling CSS from SCSS style...")
err := CompileSASS(DefaultScss...)
log.Infoln("Statping assets have been inserted")
@ -161,11 +162,8 @@ func DeleteAllAssets(folder string) error {
func CopyAllToPublic(box *rice.Box) error {
exclude := map[string]bool{
"base.gohtml": true,
"index.html": true,
"swagger.json": true,
"postman.json": true,
"grafana.json": true,
"base.gohtml": true,
"index.html": true,
}
err := box.Walk("/", func(path string, info os.FileInfo, err error) error {

View File

@ -16,6 +16,7 @@ func init() {
utils.InitLogs()
Assets()
utils.DeleteDirectory(dir + "/assets")
dir = utils.Params.GetString("STATPING_DIR")
}
func TestCore_UsingAssets(t *testing.T) {
@ -23,7 +24,7 @@ func TestCore_UsingAssets(t *testing.T) {
}
func TestCreateAssets(t *testing.T) {
assert.Nil(t, CreateAllAssets(dir))
CreateAllAssets(dir)
assert.True(t, UsingAssets(dir))
assert.Nil(t, CompileSASS(DefaultScss...))
assert.FileExists(t, dir+"/assets/css/main.css")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,611 +0,0 @@
{
"swagger": "2.0",
"info": {
"version": "1.0",
"title": "Statping",
"description": "Statping API Requests"
},
"host": "example.com",
"basePath": "/",
"securityDefinitions": {
"auth": {
"type": "oauth2",
"flow": "implicit",
"authorizationUrl": "http://example.com",
"scopes": {}
}
},
"schemes": [
"http"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/api": {
"get": {
"description": "TODO: Add Description",
"summary": "Statping Details",
"tags": [
"Main"
],
"operationId": "ApiGet",
"produces": [
"application/json"
],
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
}
},
"/api/services": {
"get": {
"description": "TODO: Add Description",
"summary": "View All Services",
"tags": [
"Services"
],
"operationId": "ApiServicesGet",
"produces": [
"application/json"
],
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
},
"post": {
"description": "TODO: Add Description",
"summary": "Create Service",
"tags": [
"Services"
],
"operationId": "ApiServicesPost",
"produces": [
"application/json"
],
"parameters": [
{
"name": "Content-Type",
"in": "header",
"required": true,
"type": "string",
"description": ""
},
{
"name": "Body",
"in": "body",
"required": true,
"description": "",
"schema": {
"$ref": "#/definitions/CreateServicerequest"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
}
},
"/api/services/1": {
"get": {
"description": "TODO: Add Description",
"summary": "View Service",
"tags": [
"Services"
],
"operationId": "ApiServices1Get",
"produces": [
"application/json"
],
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
},
"delete": {
"description": "TODO: Add Description",
"summary": "Delete Service",
"tags": [
"Services"
],
"operationId": "ApiServices1Delete",
"produces": [
"application/json"
],
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
}
},
"/api/services/19": {
"post": {
"description": "TODO: Add Description",
"summary": "Update Service",
"tags": [
"Services"
],
"operationId": "ApiServices19Post",
"produces": [
"application/json"
],
"parameters": [
{
"name": "Content-Type",
"in": "header",
"required": true,
"type": "string",
"description": ""
},
{
"name": "Body",
"in": "body",
"required": true,
"description": "",
"schema": {
"$ref": "#/definitions/UpdateServicerequest"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
}
},
"/api/users": {
"get": {
"description": "TODO: Add Description",
"summary": "View All Users",
"tags": [
"Users"
],
"operationId": "ApiUsersGet",
"produces": [
"application/json"
],
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
},
"post": {
"description": "TODO: Add Description",
"summary": "Create User",
"tags": [
"Users"
],
"operationId": "ApiUsersPost",
"produces": [
"application/json"
],
"parameters": [
{
"name": "Content-Type",
"in": "header",
"required": true,
"type": "string",
"description": ""
},
{
"name": "Body",
"in": "body",
"required": true,
"description": "",
"schema": {
"$ref": "#/definitions/CreateUserrequest"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
}
},
"/api/users/1": {
"get": {
"description": "TODO: Add Description",
"summary": "View User",
"tags": [
"Users"
],
"operationId": "ApiUsers1Get",
"produces": [
"application/json"
],
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
}
},
"/api/users/4": {
"post": {
"description": "TODO: Add Description",
"summary": "Update User",
"tags": [
"Users"
],
"operationId": "ApiUsers4Post",
"produces": [
"application/json"
],
"parameters": [
{
"name": "Content-Type",
"in": "header",
"required": true,
"type": "string",
"description": ""
},
{
"name": "Body",
"in": "body",
"required": true,
"description": "",
"schema": {
"$ref": "#/definitions/UpdateUserrequest"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
},
"delete": {
"description": "TODO: Add Description",
"summary": "Delete User",
"tags": [
"Users"
],
"operationId": "ApiUsers4Delete",
"produces": [
"application/json"
],
"parameters": [],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"auth": []
}
]
}
}
},
"definitions": {
"CreateServicerequest": {
"title": "Create ServiceRequest",
"example": {
"name": "New Service",
"domain": "https://google.com",
"expected": "",
"expected_status": 200,
"check_interval": 15,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 10,
"order_id": 0
},
"type": "object",
"properties": {
"name": {
"description": "",
"example": "New Service",
"type": "string"
},
"domain": {
"description": "",
"example": "https://google.com",
"type": "string"
},
"expected": {
"description": "",
"type": "string"
},
"expected_status": {
"description": "",
"example": 200,
"type": "integer",
"format": "int32"
},
"check_interval": {
"description": "",
"example": 15,
"type": "integer",
"format": "int32"
},
"type": {
"description": "",
"example": "http",
"type": "string"
},
"method": {
"description": "",
"example": "GET",
"type": "string"
},
"post_data": {
"description": "",
"type": "string"
},
"port": {
"description": "",
"example": 0,
"type": "integer",
"format": "int32"
},
"timeout": {
"description": "",
"example": 10,
"type": "integer",
"format": "int32"
},
"order_id": {
"description": "",
"example": 0,
"type": "integer",
"format": "int32"
}
},
"required": [
"name",
"domain",
"expected",
"expected_status",
"check_interval",
"type",
"method",
"post_data",
"port",
"timeout",
"order_id"
]
},
"UpdateServicerequest": {
"title": "Update ServiceRequest",
"example": {
"name": "Updated Service",
"domain": "https://google.com",
"expected": "",
"expected_status": 200,
"check_interval": 60,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 10,
"order_id": 0
},
"type": "object",
"properties": {
"name": {
"description": "",
"example": "Updated Service",
"type": "string"
},
"domain": {
"description": "",
"example": "https://google.com",
"type": "string"
},
"expected": {
"description": "",
"type": "string"
},
"expected_status": {
"description": "",
"example": 200,
"type": "integer",
"format": "int32"
},
"check_interval": {
"description": "",
"example": 60,
"type": "integer",
"format": "int32"
},
"type": {
"description": "",
"example": "http",
"type": "string"
},
"method": {
"description": "",
"example": "GET",
"type": "string"
},
"post_data": {
"description": "",
"type": "string"
},
"port": {
"description": "",
"example": 0,
"type": "integer",
"format": "int32"
},
"timeout": {
"description": "",
"example": 10,
"type": "integer",
"format": "int32"
},
"order_id": {
"description": "",
"example": 0,
"type": "integer",
"format": "int32"
}
},
"required": [
"name",
"domain",
"expected",
"expected_status",
"check_interval",
"type",
"method",
"post_data",
"port",
"timeout",
"order_id"
]
},
"CreateUserrequest": {
"title": "Create UserRequest",
"example": {
"username": "admin",
"email": "info@email.com",
"password": "password123",
"admin": true
},
"type": "object",
"properties": {
"username": {
"description": "",
"example": "admin",
"type": "string"
},
"email": {
"description": "",
"example": "info@email.com",
"type": "string"
},
"password": {
"description": "",
"example": "password123",
"type": "string"
},
"admin": {
"description": "",
"example": true,
"type": "boolean"
}
},
"required": [
"username",
"email",
"password",
"admin"
]
},
"UpdateUserrequest": {
"title": "Update UserRequest",
"example": {
"username": "adminupdated",
"email": "info@email.com",
"password": "password123",
"admin": true
},
"type": "object",
"properties": {
"username": {
"description": "",
"example": "adminupdated",
"type": "string"
},
"email": {
"description": "",
"example": "info@email.com",
"type": "string"
},
"password": {
"description": "",
"example": "password123",
"type": "string"
},
"admin": {
"description": "",
"example": true,
"type": "boolean"
}
},
"required": [
"username",
"email",
"password",
"admin"
]
}
}
}

View File

@ -15,7 +15,10 @@ func (c *Checkin) Expected() time.Duration {
}
func (c *Checkin) Period() time.Duration {
duration, _ := time.ParseDuration(fmt.Sprintf("%vs", c.Interval))
duration, _ := time.ParseDuration(fmt.Sprintf("%ds", c.Interval))
if duration.Seconds() <= 15 {
return 15 * time.Second
}
return duration
}

View File

@ -38,18 +38,17 @@ CheckinLoop:
c.Failing = false
break CheckinLoop
case <-time.After(reCheck):
log.Infoln(fmt.Sprintf("Checkin %v is expected at %v, checking every %v", c.Name, utils.FormatDuration(c.Expected()), utils.FormatDuration(c.Period())))
log.Infoln(fmt.Sprintf("Checkin '%s' expects a request every %v", c.Name, utils.FormatDuration(c.Period())))
if c.Expected() <= 0 {
issue := fmt.Sprintf("Checkin %v is failing, no request since %v", c.Name, lastHit.CreatedAt)
log.Errorln(issue)
issue := fmt.Sprintf("Checkin '%s' is failing, no request since %v", c.Name, lastHit.CreatedAt)
//log.Errorln(issue)
fail := &failures.Failure{
Issue: issue,
Method: "checkin",
Service: c.ServiceId,
Checkin: c.Id,
PingTime: c.Expected().Milliseconds(),
CreatedAt: time.Time{},
Issue: issue,
Method: "checkin",
Service: c.ServiceId,
Checkin: c.Id,
PingTime: c.Expected().Milliseconds(),
}
c.CreateFailure(fail)

View File

@ -31,7 +31,7 @@ func Samples() error {
}
func SamplesChkHits() error {
checkTime := time.Now().UTC().Add(-24 * time.Hour)
checkTime := utils.Now().Add(-3 * time.Minute)
for i := int64(1); i <= 2; i++ {
checkHit := &CheckinHit{
@ -44,7 +44,7 @@ func SamplesChkHits() error {
return err
}
checkTime = checkTime.Add(10 * time.Minute)
checkTime = checkTime.Add(1 * time.Minute)
}
return nil

View File

@ -32,6 +32,8 @@ type CheckinHit struct {
}
func (c *Checkin) BeforeCreate() (err error) {
c.ApiKey = utils.RandomString(7)
if c.ApiKey == "" {
c.ApiKey = utils.RandomString(7)
}
return nil
}

View File

@ -0,0 +1,48 @@
package configs
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestSQLiteConfig(t *testing.T) {
sqlite := &DbConfig{
DbConn: "sqlite",
DbHost: "localhost",
DbUser: "",
DbPass: "",
DbData: "",
DbPort: 0,
}
err := Connect(sqlite, false)
require.Nil(t, err)
}
func TestMySQLConfig(t *testing.T) {
mysql := &DbConfig{
DbConn: "mysql",
DbHost: "localhost",
DbUser: "root",
DbPass: "password123",
DbData: "statping",
DbPort: 3306,
}
err := Connect(mysql, false)
require.Nil(t, err)
}
func TestPostgresConfig(t *testing.T) {
postgres := &DbConfig{
DbConn: "postgres",
DbHost: "localhost",
DbUser: "root",
DbPass: "password123",
DbData: "statping",
DbPort: 5432,
}
err := Connect(postgres, false)
require.Nil(t, err)
}

View File

@ -1,78 +0,0 @@
package configs
import (
"github.com/joho/godotenv"
"github.com/pkg/errors"
"github.com/statping/statping/utils"
)
func loadConfigEnvs() (*DbConfig, error) {
var err error
log.Infof("Loading configs from environment variables")
loadDotEnvs()
dbConn := utils.Getenv("DB_CONN", "").(string)
dbHost := utils.Getenv("DB_HOST", "").(string)
dbUser := utils.Getenv("DB_USER", "").(string)
dbPass := utils.Getenv("DB_PASS", "").(string)
dbData := utils.Getenv("DB_DATABASE", "").(string)
dbPort := utils.Getenv("DB_PORT", defaultPort(dbConn)).(int)
name := utils.Getenv("NAME", "Statping").(string)
desc := utils.Getenv("DESCRIPTION", "Statping Monitoring Sample Data").(string)
user := utils.Getenv("ADMIN_USER", "admin").(string)
password := utils.Getenv("ADMIN_PASS", "admin").(string)
domain := utils.Getenv("DOMAIN", "").(string)
sqlFile := utils.Getenv("SQL_FILE", "").(string)
if dbConn != "" && dbConn != "sqlite" {
if dbHost == "" {
return nil, errors.New("Missing DB_HOST environment variable")
}
if dbPort == 0 {
return nil, errors.New("Missing DB_PORT environment variable")
}
if dbUser == "" {
return nil, errors.New("Missing DB_USER environment variable")
}
if dbPass == "" {
return nil, errors.New("Missing DB_PASS environment variable")
}
if dbData == "" {
return nil, errors.New("Missing DB_DATABASE environment variable")
}
}
config := &DbConfig{
DbConn: dbConn,
DbHost: dbHost,
DbUser: dbUser,
DbPass: dbPass,
DbData: dbData,
DbPort: dbPort,
Project: name,
Description: desc,
Domain: domain,
Email: "",
Username: user,
Password: password,
Error: nil,
Location: utils.Directory,
SqlFile: sqlFile,
}
return config, err
}
// loadDotEnvs attempts to load database configs from a '.env' file in root directory
func loadDotEnvs() {
err := godotenv.Overload(utils.Directory + "/" + ".env")
if err == nil {
log.Warnln("Environment file '.env' found")
envs, _ := godotenv.Read(utils.Directory + "/" + ".env")
for k, e := range envs {
log.Infof("Overwriting %s=%s\n", k, e)
}
log.Warnln("These environment variables will overwrite any existing")
}
}

View File

@ -1,26 +0,0 @@
package configs
import (
"github.com/pkg/errors"
"github.com/statping/statping/types/core"
"github.com/statping/statping/utils"
"gopkg.in/yaml.v2"
)
func LoadConfigFile(directory string) (*DbConfig, error) {
var configs *DbConfig
log.Infof("Attempting to read config file at: %s/config.yml ", directory)
file, err := utils.OpenFile(directory + "/config.yml")
if err != nil {
core.App.Setup = false
return nil, errors.Wrapf(err, "config.yml file not found at %s/config.yml - starting in setup mode", directory)
}
err = yaml.Unmarshal([]byte(file), &configs)
if err != nil {
return nil, errors.Wrap(err, "yaml file not formatted correctly")
}
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + directory + "/config.yml")
return configs, nil
}

View File

@ -10,24 +10,35 @@ func LoadConfigForm(r *http.Request) (*DbConfig, error) {
if err := r.ParseForm(); err != nil {
return nil, err
}
dbHost := r.PostForm.Get("db_host")
dbUser := r.PostForm.Get("db_user")
dbPass := r.PostForm.Get("db_password")
dbDatabase := r.PostForm.Get("db_database")
dbConn := r.PostForm.Get("db_connection")
dbPort := utils.ToInt(r.PostForm.Get("db_port"))
project := r.PostForm.Get("project")
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
description := r.PostForm.Get("description")
domain := r.PostForm.Get("domain")
email := r.PostForm.Get("email")
g := r.PostForm.Get
dbHost := g("db_host")
dbUser := g("db_user")
dbPass := g("db_password")
dbDatabase := g("db_database")
dbConn := g("db_connection")
dbPort := utils.ToInt(g("db_port"))
project := g("project")
username := g("username")
password := g("password")
description := g("description")
domain := g("domain")
email := g("email")
if project == "" || username == "" || password == "" {
err := errors.New("Missing required elements on setup form")
return nil, err
}
p := utils.Params
p.Set("DB_CONN", dbConn)
p.Set("DB_HOST", dbHost)
p.Set("DB_USER", dbUser)
p.Set("DB_PORT", dbPort)
p.Set("DB_PASS", dbPass)
p.Set("DB_DATABASE", dbDatabase)
p.Set("NAME", project)
p.Set("DESCRIPTION", description)
confg := &DbConfig{
DbConn: dbConn,
DbHost: dbHost,
@ -41,7 +52,6 @@ func LoadConfigForm(r *http.Request) (*DbConfig, error) {
Username: username,
Password: password,
Email: email,
Error: nil,
Location: utils.Directory,
}

View File

@ -17,45 +17,20 @@ import (
"github.com/statping/statping/types/services"
"github.com/statping/statping/types/users"
"github.com/statping/statping/utils"
"os"
"time"
)
// Connect will attempt to connect to the sqlite, postgres, or mysql database
func Connect(configs *DbConfig, retry bool) error {
postgresSSL := os.Getenv("POSTGRES_SSLMODE")
var conn string
conn := configs.ConnectionString()
p := utils.Params
var err error
switch configs.DbConn {
case "sqlite", "sqlite3", "memory":
if configs.DbConn == "memory" {
conn = "sqlite3"
configs.DbConn = ":memory:"
} else {
conn = findDbFile(configs)
configs.SqlFile = conn
log.Infof("SQL database file at: %s", configs.SqlFile)
configs.DbConn = "sqlite3"
}
case "mysql":
host := fmt.Sprintf("%v:%v", configs.DbHost, configs.DbPort)
conn = fmt.Sprintf("%v:%v@tcp(%v)/%v?charset=utf8&parseTime=True&loc=UTC&time_zone=%%27UTC%%27", configs.DbUser, configs.DbPass, host, configs.DbData)
case "postgres":
sslMode := "disable"
if postgresSSL != "" {
sslMode = postgresSSL
}
conn = fmt.Sprintf("host=%v port=%v user=%v dbname=%v password=%v timezone=UTC sslmode=%v", configs.DbHost, configs.DbPort, configs.DbUser, configs.DbData, configs.DbPass, sslMode)
case "mssql":
host := fmt.Sprintf("%v:%v", configs.DbHost, configs.DbPort)
conn = fmt.Sprintf("sqlserver://%v:%v@%v?database=%v", configs.DbUser, configs.DbPass, host, configs.DbData)
}
log.WithFields(utils.ToFields(configs, conn)).Debugln("attempting to connect to database")
dbSession, err := database.Openw(configs.DbConn, conn)
if err != nil {
log.Debugln(fmt.Sprintf("Database connection error %s", err))
log.Errorf(fmt.Sprintf("Database connection error %s", err))
if retry {
log.Warnln(fmt.Sprintf("Database %s connection to '%s' is not available, trying again in 5 seconds...", configs.DbConn, configs.DbHost))
time.Sleep(5 * time.Second)
@ -65,20 +40,20 @@ func Connect(configs *DbConfig, retry bool) error {
}
}
apiKey := utils.Getenv("API_KEY", utils.RandomString(16)).(string)
apiSecret := utils.Getenv("API_SECRET", utils.RandomString(16)).(string)
apiKey := p.GetString("API_KEY")
apiSecret := p.GetString("API_SECRET")
configs.ApiKey = apiKey
configs.ApiSecret = apiSecret
log.WithFields(utils.ToFields(dbSession)).Debugln("connected to database")
maxOpenConn := utils.Getenv("MAX_OPEN_CONN", 25)
maxIdleConn := utils.Getenv("MAX_IDLE_CONN", 25)
maxLifeConn := utils.Getenv("MAX_LIFE_CONN", 5*time.Minute)
maxOpenConn := p.GetInt("MAX_OPEN_CONN")
maxIdleConn := p.GetInt("MAX_IDLE_CONN")
maxLifeConn := p.GetDuration("MAX_LIFE_CONN")
dbSession.DB().SetMaxOpenConns(maxOpenConn.(int))
dbSession.DB().SetMaxIdleConns(maxIdleConn.(int))
dbSession.DB().SetConnMaxLifetime(maxLifeConn.(time.Duration))
dbSession.DB().SetMaxOpenConns(maxOpenConn)
dbSession.DB().SetMaxIdleConns(maxIdleConn)
dbSession.DB().SetConnMaxLifetime(maxLifeConn)
if dbSession.DB().Ping() == nil {
if utils.VerboseMode >= 4 {
@ -107,17 +82,20 @@ func initModels(db database.Database) {
groups.SetDB(db)
}
func CreateAdminUser(configs *DbConfig) error {
log.Infoln(fmt.Sprintf("Core database does not exist, creating now!"))
func CreateAdminUser(c *DbConfig) error {
log.Infoln(fmt.Sprintf("Default Admininstrator user does not exist, creating now! (admin/admin)"))
if configs.Username == "" && configs.Password == "" {
configs.Username = utils.Getenv("ADMIN_USER", "admin").(string)
configs.Password = utils.Getenv("ADMIN_PASSWORD", "admin").(string)
adminUser := utils.Params.GetString("ADMIN_USER")
adminPass := utils.Params.GetString("ADMIN_PASSWORD")
if adminUser == "" || adminPass == "" {
adminUser = "admin"
adminPass = "admin"
}
admin := &users.User{
Username: configs.Username,
Password: configs.Password,
Username: adminUser,
Password: adminPass,
Email: "info@admin.com",
Admin: null.NewNullBool(true),
}

View File

@ -1,6 +1,8 @@
package configs
import (
"fmt"
"github.com/pkg/errors"
"github.com/statping/statping/database"
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/core"
@ -101,11 +103,11 @@ func (d *DbConfig) CreateDatabase() error {
log.Infoln("Creating Database Tables...")
for _, table := range DbModels {
if err := d.Db.CreateTable(table); err.Error() != nil {
return err.Error()
return errors.Wrap(err.Error(), fmt.Sprintf("error creating '%T' table", table))
}
}
if err := d.Db.Table("core").CreateTable(&core.Core{}); err.Error() != nil {
return err.Error()
return errors.Wrap(err.Error(), fmt.Sprintf("error creating 'core' table"))
}
log.Infoln("Statping Database Created")

View File

@ -1,24 +1,23 @@
package configs
import (
"fmt"
"github.com/pkg/errors"
"github.com/statping/statping/utils"
"os"
"path/filepath"
"strings"
)
var log = utils.Log
func ConnectConfigs(configs *DbConfig) error {
err := Connect(configs, true)
func ConnectConfigs(configs *DbConfig, retry bool) error {
err := Connect(configs, retry)
if err != nil {
return errors.Wrap(err, "error connecting to database")
}
if err := configs.Save(utils.Directory); err != nil {
return errors.Wrap(err, "error saving configuration")
}
return nil
}
@ -31,42 +30,43 @@ func LoadConfigs() (*DbConfig, error) {
return nil, errors.Errorf("Directory %s is not writable!", utils.Directory)
}
dbConn := utils.Getenv("DB_CONN", "").(string)
if dbConn != "" {
configs, err := loadConfigEnvs()
if err != nil {
return LoadConfigFile(utils.Directory)
}
return configs, nil
}
return LoadConfigFile(utils.Directory)
}
func findDbFile(configs *DbConfig) string {
func findDbFile(configs *DbConfig) (string, error) {
location := utils.Directory + "/" + SqliteFilename
if configs == nil {
return findSQLin(utils.Directory)
file, err := findSQLin(utils.Directory)
if err != nil {
log.Errorln(err)
return location, nil
}
location = file
}
if configs.SqlFile != "" {
return configs.SqlFile
if configs != nil && configs.SqlFile != "" {
return configs.SqlFile, nil
}
return utils.Directory + "/" + SqliteFilename
return location, nil
}
func findSQLin(path string) string {
func findSQLin(path string) (string, error) {
filename := SqliteFilename
var found []string
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if filepath.Ext(path) == ".db" {
fmt.Println("DB file is now: ", info.Name())
filename = info.Name()
found = append(found, filename)
}
return nil
})
if err != nil {
log.Error(err)
return filename, err
}
return filename
if len(found) > 1 {
return filename, errors.Errorf("found multiple database files: %s", strings.Join(found, ", "))
}
return filename, nil
}

View File

@ -12,58 +12,58 @@ func init() {
os.Setenv("MIGRATION_ID", utils.ToString(latestMigration))
}
func (c *DbConfig) genericMigration(alterStr string, isPostgres bool) error {
func (d *DbConfig) genericMigration(alterStr string, isPostgres bool) error {
var extra string
extraType := "UNSIGNED INTEGER"
if isPostgres {
extra = " TYPE"
extraType = "bigint"
}
if err := c.Db.Exec(fmt.Sprintf("ALTER TABLE hits %s COLUMN latency%s BIGINT;", alterStr, extra)).Error(); err != nil {
if err := d.Db.Exec(fmt.Sprintf("ALTER TABLE hits %s COLUMN latency%s BIGINT;", alterStr, extra)).Error(); err != nil {
return err
}
if err := c.Db.Exec(fmt.Sprintf("ALTER TABLE hits %s COLUMN ping_time%s BIGINT;", alterStr, extra)).Error(); err != nil {
if err := d.Db.Exec(fmt.Sprintf("ALTER TABLE hits %s COLUMN ping_time%s BIGINT;", alterStr, extra)).Error(); err != nil {
return err
}
if err := c.Db.Exec(fmt.Sprintf("ALTER TABLE failures %s COLUMN ping_time%s BIGINT;", alterStr, extra)).Error(); err != nil {
if err := d.Db.Exec(fmt.Sprintf("ALTER TABLE failures %s COLUMN ping_time%s BIGINT;", alterStr, extra)).Error(); err != nil {
return err
}
if err := c.Db.Exec(fmt.Sprintf("UPDATE hits SET latency = CAST(latency * 1000000 AS %s);", extraType)).Error(); err != nil {
if err := d.Db.Exec(fmt.Sprintf("UPDATE hits SET latency = CAST(latency * 1000000 AS %s);", extraType)).Error(); err != nil {
return err
}
if err := c.Db.Exec(fmt.Sprintf("UPDATE hits SET ping_time = CAST(ping_time * 1000000 AS %s);", extraType)).Error(); err != nil {
if err := d.Db.Exec(fmt.Sprintf("UPDATE hits SET ping_time = CAST(ping_time * 1000000 AS %s);", extraType)).Error(); err != nil {
return err
}
if err := c.Db.Exec(fmt.Sprintf("UPDATE failures SET ping_time = CAST(ping_time * 1000000 AS %s);", extraType)).Error(); err != nil {
if err := d.Db.Exec(fmt.Sprintf("UPDATE failures SET ping_time = CAST(ping_time * 1000000 AS %s);", extraType)).Error(); err != nil {
return err
}
return nil
}
func (c *DbConfig) sqliteMigration() error {
if err := c.Db.Exec(`ALTER TABLE hits RENAME TO hits_backup;`).Error(); err != nil {
func (d *DbConfig) sqliteMigration() error {
if err := d.Db.Exec(`ALTER TABLE hits RENAME TO hits_backup;`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`CREATE TABLE hits (id INTEGER PRIMARY KEY AUTOINCREMENT, service bigint, latency bigint, ping_time bigint, created_at datetime);`).Error(); err != nil {
if err := d.Db.Exec(`CREATE TABLE hits (id INTEGER PRIMARY KEY AUTOINCREMENT, service bigint, latency bigint, ping_time bigint, created_at datetime);`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`INSERT INTO hits (id, service, latency, ping_time, created_at) SELECT id, service, CAST(latency * 1000000 AS bigint), CAST(ping_time * 1000000 AS bigint), created_at FROM hits_backup;`).Error(); err != nil {
if err := d.Db.Exec(`INSERT INTO hits (id, service, latency, ping_time, created_at) SELECT id, service, CAST(latency * 1000000 AS bigint), CAST(ping_time * 1000000 AS bigint), created_at FROM hits_backup;`).Error(); err != nil {
return err
}
// failures table
if err := c.Db.Exec(`ALTER TABLE failures RENAME TO failures_backup;`).Error(); err != nil {
if err := d.Db.Exec(`ALTER TABLE failures RENAME TO failures_backup;`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`CREATE TABLE failures (id INTEGER PRIMARY KEY AUTOINCREMENT, issue varchar(255), method varchar(255), method_id bigint, service bigint, ping_time bigint, checkin bigint, error_code bigint, created_at datetime);`).Error(); err != nil {
if err := d.Db.Exec(`CREATE TABLE failures (id INTEGER PRIMARY KEY AUTOINCREMENT, issue varchar(255), method varchar(255), method_id bigint, service bigint, ping_time bigint, checkin bigint, error_code bigint, created_at datetime);`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`INSERT INTO failures (id, issue, method, method_id, service, ping_time, checkin, created_at) SELECT id, issue, method, method_id, service, CAST(ping_time * 1000000 AS bigint), checkin, created_at FROM failures_backup;`).Error(); err != nil {
if err := d.Db.Exec(`INSERT INTO failures (id, issue, method, method_id, service, ping_time, checkin, created_at) SELECT id, issue, method, method_id, service, CAST(ping_time * 1000000 AS bigint), checkin, created_at FROM failures_backup;`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`DROP TABLE hits_backup;`).Error(); err != nil {
if err := d.Db.Exec(`DROP TABLE hits_backup;`).Error(); err != nil {
return err
}
if err := c.Db.Exec(`DROP TABLE failures_backup;`).Error(); err != nil {
if err := d.Db.Exec(`DROP TABLE failures_backup;`).Error(); err != nil {
return err
}
return nil

78
types/configs/load.go Normal file
View File

@ -0,0 +1,78 @@
package configs
import (
"github.com/statping/statping/types/errors"
"github.com/statping/statping/utils"
"gopkg.in/yaml.v2"
"os"
)
func LoadConfigFile(directory string) (*DbConfig, error) {
p := utils.Params
log.Infof("Attempting to read config file at: %s/config.yml ", directory)
p.SetConfigFile(directory + "/config.yml")
p.SetConfigType("yaml")
p.ReadInConfig()
db := new(DbConfig)
content, err := utils.OpenFile(directory + "/config.yml")
if err == nil {
if err := yaml.Unmarshal([]byte(content), &db); err != nil {
return nil, err
}
}
if os.Getenv("DB_CONN") == "sqlite" || os.Getenv("DB_CONN") == "sqlite3" {
db.DbConn = "sqlite3"
}
if db.DbConn != "" {
p.Set("DB_CONN", db.DbConn)
}
if db.DbHost != "" {
p.Set("DB_HOST", db.DbHost)
}
if db.DbPort != 0 {
p.Set("DB_PORT", db.DbPort)
}
if db.DbPass != "" {
p.Set("DB_PASS", db.DbPass)
}
if db.DbUser != "" {
p.Set("DB_USER", db.DbUser)
}
if db.DbData != "" {
p.Set("DB_DATABASE", db.DbData)
}
if db.Location != "" {
p.Set("LOCATION", db.Location)
}
if db.ApiKey != "" {
p.Set("API_KEY", db.ApiKey)
}
if db.ApiSecret != "" {
p.Set("API_SECRET", db.ApiSecret)
}
configs := &DbConfig{
DbConn: p.GetString("DB_CONN"),
DbHost: p.GetString("DB_HOST"),
DbUser: p.GetString("DB_USER"),
DbPass: p.GetString("DB_PASS"),
DbData: p.GetString("DB_DATABASE"),
DbPort: p.GetInt("DB_PORT"),
Project: p.GetString("NAME"),
Description: p.GetString("DESCRIPTION"),
Domain: p.GetString("DOMAIN"),
Email: p.GetString("EMAIL"),
Username: p.GetString("ADMIN_USER"),
Password: p.GetString("ADMIN_PASS"),
Location: utils.Directory,
SqlFile: p.GetString("SQL_FILE"),
}
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + directory + "/config.yml")
if configs.DbConn == "" {
return configs, errors.New("Starting in setup mode")
}
return configs, nil
}

View File

@ -1,34 +1,48 @@
package configs
import (
"fmt"
"github.com/statping/statping/utils"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
)
// Save will initially create the config.yml file
func (d *DbConfig) Save(directory string) error {
data, err := yaml.Marshal(d)
c, err := yaml.Marshal(d)
if err != nil {
return err
}
if err := ioutil.WriteFile(directory+"/config.yml", data, os.ModePerm); err != nil {
return err
if err := utils.SaveFile(directory+"/config.yml", c); err != nil {
return nil
}
d.filename = directory + "/config.yml"
return nil
}
// defaultPort accepts a database type and returns its default port
func defaultPort(db string) int {
switch db {
func (d *DbConfig) ConnectionString() string {
var conn string
postgresSSL := utils.Params.GetString("POSTGRES_SSLMODE")
switch d.DbConn {
case "memory", ":memory:":
conn = "sqlite3"
d.DbConn = ":memory:"
return d.DbConn
case "sqlite", "sqlite3":
conn, err := findDbFile(d)
if err != nil {
log.Errorln(err)
}
d.SqlFile = conn
log.Infof("SQL database file at: %s", d.SqlFile)
d.DbConn = "sqlite3"
return d.SqlFile
case "mysql":
return 3306
host := fmt.Sprintf("%v:%v", d.DbHost, d.DbPort)
conn = fmt.Sprintf("%v:%v@tcp(%v)/%v?charset=utf8&parseTime=True&loc=UTC&time_zone=%%27UTC%%27", d.DbUser, d.DbPass, host, d.DbData)
return conn
case "postgres":
return 5432
case "mssql":
return 1433
default:
return 0
conn = fmt.Sprintf("host=%v port=%v user=%v dbname=%v password=%v timezone=UTC sslmode=%v", d.DbHost, d.DbPort, d.DbUser, d.DbData, d.DbPass, postgresSSL)
return conn
}
return conn
}

View File

@ -20,33 +20,33 @@ import (
"github.com/statping/statping/types/users"
)
func (c *DbConfig) DatabaseChanges() error {
func (d *DbConfig) DatabaseChanges() error {
var cr core.Core
c.Db.Model(&core.Core{}).Find(&cr)
d.Db.Model(&core.Core{}).Find(&cr)
if latestMigration > cr.MigrationId {
log.Infof("Statping database is out of date, migrating to: %d", latestMigration)
switch c.Db.DbType() {
switch d.Db.DbType() {
case "mysql":
if err := c.genericMigration("MODIFY", false); err != nil {
if err := d.genericMigration("MODIFY", false); err != nil {
return err
}
case "postgres":
if err := c.genericMigration("ALTER", true); err != nil {
if err := d.genericMigration("ALTER", true); err != nil {
return err
}
default:
if err := c.sqliteMigration(); err != nil {
if err := d.sqliteMigration(); err != nil {
return err
}
}
if err := c.Db.Exec(fmt.Sprintf("UPDATE core SET migration_id = %d", latestMigration)).Error(); err != nil {
if err := d.Db.Exec(fmt.Sprintf("UPDATE core SET migration_id = %d", latestMigration)).Error(); err != nil {
return err
}
if err := c.BackupAssets(); err != nil {
if err := d.BackupAssets(); err != nil {
return err
}
}
@ -55,7 +55,7 @@ func (c *DbConfig) DatabaseChanges() error {
// BackupAssets is a temporary function (to version 0.90.*) to backup your customized theme
// to a new folder called 'assets_backup'.
func (c *DbConfig) BackupAssets() error {
func (d *DbConfig) BackupAssets() error {
if source.UsingAssets(utils.Directory) {
log.Infof("Backing up 'assets' folder to 'assets_backup'")
if err := utils.RenameDirectory(utils.Directory+"/assets", utils.Directory+"/assets_backup"); err != nil {
@ -69,12 +69,12 @@ func (c *DbConfig) BackupAssets() error {
//MigrateDatabase will migrate the database structure to current version.
//This function will NOT remove previous records, tables or columns from the database.
//If this function has an issue, it will ROLLBACK to the previous state.
func (c *DbConfig) MigrateDatabase() error {
func (d *DbConfig) MigrateDatabase() error {
var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &notifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}}
log.Infoln("Migrating Database Tables...")
tx := c.Db.Begin()
tx := d.Db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
@ -99,27 +99,27 @@ func (c *DbConfig) MigrateDatabase() error {
return err
}
c.Db.Table("core").Model(&core.Core{}).Update("version", core.App.Version)
d.Db.Table("core").Model(&core.Core{}).Update("version", core.App.Version)
log.Infoln("Statping Database Tables Migrated")
if err := c.Db.Model(&hits.Hit{}).AddIndex("idx_service_hit", "service").Error(); err != nil {
if err := d.Db.Model(&hits.Hit{}).AddIndex("idx_service_hit", "service").Error(); err != nil {
log.Errorln(err)
}
if err := c.Db.Model(&hits.Hit{}).AddIndex("hit_created_at", "created_at").Error(); err != nil {
if err := d.Db.Model(&hits.Hit{}).AddIndex("hit_created_at", "created_at").Error(); err != nil {
log.Errorln(err)
}
if err := c.Db.Model(&failures.Failure{}).AddIndex("fail_created_at", "created_at").Error(); err != nil {
if err := d.Db.Model(&failures.Failure{}).AddIndex("fail_created_at", "created_at").Error(); err != nil {
log.Errorln(err)
}
if err := c.Db.Model(&failures.Failure{}).AddIndex("idx_service_fail", "service").Error(); err != nil {
if err := d.Db.Model(&failures.Failure{}).AddIndex("idx_service_fail", "service").Error(); err != nil {
log.Errorln(err)
}
if err := c.Db.Model(&failures.Failure{}).AddIndex("idx_checkin_fail", "checkin").Error(); err != nil {
if err := d.Db.Model(&failures.Failure{}).AddIndex("idx_checkin_fail", "checkin").Error(); err != nil {
log.Errorln(err)
}
log.Infoln("Database Indexes Created")

View File

@ -5,8 +5,6 @@ import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/null"
"github.com/statping/statping/utils"
"os"
"time"
)
var db database.Database
@ -31,32 +29,29 @@ func Select() (*Core, error) {
}
App = &c
if os.Getenv("USE_CDN") == "true" {
if utils.Params.GetBool("USE_CDN") {
App.UseCdn = null.NewNullBool(true)
}
if os.Getenv("ALLOW_REPORTS") == "true" {
if utils.Params.GetBool("ALLOW_REPORTS") {
App.AllowReports = null.NewNullBool(true)
}
return App, q.Error()
}
func (c *Core) Create() error {
apiKey := utils.Getenv("API_KEY", utils.NewSHA256Hash()).(string)
apiSecret := utils.Getenv("API_SECRET", utils.NewSHA256Hash()).(string)
if c.ApiKey == "" || c.ApiSecret == "" {
c.ApiSecret = apiSecret
c.ApiKey = apiKey
secret := utils.Params.GetString("API_SECRET")
if secret == "" {
secret = utils.RandomString(32)
}
newCore := &Core{
Name: c.Name,
Description: c.Description,
ConfigFile: utils.Directory + "/config.yml",
ApiKey: c.ApiKey,
ApiSecret: c.ApiSecret,
ApiKey: utils.RandomString(32),
ApiSecret: secret,
Version: App.Version,
Domain: c.Domain,
MigrationId: time.Now().Unix(),
MigrationId: utils.Now().Unix(),
}
q := db.Create(&newCore)
return q.Error()

View File

@ -6,14 +6,19 @@ import (
)
func Samples() error {
apiKey := utils.Getenv("API_KEY", utils.RandomString(16))
apiSecret := utils.Getenv("API_SECRET", utils.RandomString(16))
apiKey := utils.Params.GetString("API_KEY")
apiSecret := utils.Params.GetString("API_SECRET")
if apiKey == "" || apiSecret == "" {
apiKey = utils.RandomString(32)
apiSecret = utils.RandomString(32)
}
core := &Core{
Name: "Statping Sample Data",
Description: "This data is only used to testing",
ApiKey: apiKey.(string),
ApiSecret: apiSecret.(string),
ApiKey: apiKey,
ApiSecret: apiSecret,
Domain: "http://localhost:8080",
CreatedAt: utils.Now(),
UseCdn: null.NewNullBool(false),

View File

@ -34,26 +34,26 @@ type Core struct {
UseCdn null.NullBool `gorm:"column:use_cdn;default:false" json:"using_cdn,omitempty"`
LoggedIn bool `gorm:"-" json:"logged_in"`
IsAdmin bool `gorm:"-" json:"admin"`
AllowReports null.NullBool `gorm:"column:allow_reports;default:false" json:"allow_reports"`
AllowReports null.NullBool `gorm:"column:allow_reports;default:false" json:"allow_reports,omitempty"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Started time.Time `gorm:"-" json:"started_on"`
Notifications []AllNotifiers `gorm:"-" json:"-"`
Integrations []Integrator `gorm:"-" json:"-"`
OAuth `json:"oauth"`
OAuth `json:"-"`
}
type OAuth struct {
Domains string `gorm:"column:oauth_domains" json:"oauth_domains,omitempty" scope:"admin"`
Providers string `gorm:"column:oauth_providers;default:local" json:"oauth_providers,omitempty"`
GithubClientID string `gorm:"column:gh_client_id" json:"gh_client_id,omitempty" scope:"admin"`
GithubClientSecret string `gorm:"column:gh_client_secret" json:"gh_client_secret,omitempty" scope:"admin"`
GoogleClientID string `gorm:"column:google_client_id" json:"google_client_id,omitempty" scope:"admin"`
GoogleClientSecret string `gorm:"column:google_client_secret" json:"google_client_secret,omitempty" scope:"admin"`
SlackClientID string `gorm:"column:slack_client_id" json:"slack_client_id,omitempty" scope:"admin"`
SlackClientSecret string `gorm:"column:slack_client_secret" json:"slack_client_secret,omitempty" scope:"admin"`
SlackTeam string `gorm:"column:slack_team" json:"slack_team,omitempty" scope:"admin"`
Domains string `gorm:"column:oauth_domains" json:"oauth_domains" scope:"admin"`
Providers string `gorm:"column:oauth_providers;" json:"oauth_providers"`
GithubClientID string `gorm:"column:gh_client_id" json:"gh_client_id"`
GithubClientSecret string `gorm:"column:gh_client_secret" json:"gh_client_secret" scope:"admin"`
GoogleClientID string `gorm:"column:google_client_id" json:"google_client_id"`
GoogleClientSecret string `gorm:"column:google_client_secret" json:"google_client_secret" scope:"admin"`
SlackClientID string `gorm:"column:slack_client_id" json:"slack_client_id"`
SlackClientSecret string `gorm:"column:slack_client_secret" json:"slack_client_secret" scope:"admin"`
SlackTeam string `gorm:"column:slack_team" json:"slack_team"`
}
// AllNotifiers contains all the Notifiers loaded

38
types/errors/common.go Normal file
View File

@ -0,0 +1,38 @@
package errors
import (
"fmt"
"strings"
)
var (
NotAuthenticated = &appError{
Err: "user not authenticated",
Code: 401,
}
DecodeJSON = &appError{
Err: "could not decode incoming JSON",
Code: 422,
}
IDMissing = &appError{
Err: "ID missing in URL",
Code: 422,
}
NotNumber = &appError{
Err: "ID needs to be an integer",
Code: 422,
}
)
func Missing(object interface{}, id interface{}) error {
outErr := fmt.Errorf("%s with id %v was not found", splitVar(object), id)
return &appError{
Err: outErr.Error(),
Code: 404,
}
}
func splitVar(val interface{}) string {
s := strings.Split(fmt.Sprintf("%T", val), ".")
return strings.ToLower(s[len(s)-1])
}

48
types/errors/struct.go Normal file
View File

@ -0,0 +1,48 @@
package errors
import (
"github.com/pkg/errors"
)
type appError struct {
Err string `json:"error"`
Code int `json:"-"`
DbCode int `json:"code,omitempty"`
Id int64 `json:"id,omitempty"`
loggable bool `json:"-"`
}
type Error interface {
Error() string
Status() int
}
func New(err string) Error {
return &appError{
Err: err,
}
}
func Err(err Error) Error {
return &appError{
Err: err.Error(),
Code: err.Status(),
}
}
func Wrap(err error, message string) Error {
return &appError{
Err: errors.Wrap(err, message).Error(),
}
}
func (e *appError) Error() string {
return e.Err
}
func (e appError) Status() int {
if e.Code == 0 {
return 200
}
return e.Code
}

View File

@ -5,10 +5,6 @@ import (
"github.com/statping/statping/types"
"github.com/statping/statping/utils"
"time"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/mattn/go-sqlite3"
)
var (
@ -38,7 +34,6 @@ func Samples() error {
log.Infoln(fmt.Sprintf("Adding %v Failure records to service", 400))
for fi := 0.; fi <= float64(400); fi++ {
createdAt = createdAt.Add(35 * time.Minute)
failure := &Failure{
Service: i,
Issue: "testing right here",
@ -46,6 +41,7 @@ func Samples() error {
}
tx = tx.Create(&failure)
createdAt = createdAt.Add(35 * time.Minute)
}
if err := tx.Commit().Error(); err != nil {
log.Error(err)

View File

@ -2,6 +2,7 @@ package groups
import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/errors"
"sort"
)
@ -14,6 +15,9 @@ func SetDB(database database.Database) {
func Find(id int64) (*Group, error) {
var group Group
q := db.Where("id = ?", id).Find(&group)
if q.Error() != nil {
return nil, errors.Missing(group, id)
}
return &group, q.Error()
}

View File

@ -2,46 +2,40 @@ package hits
import (
"fmt"
"github.com/statping/statping/database"
"github.com/statping/statping/types"
"github.com/statping/statping/utils"
"sync"
"time"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/mattn/go-sqlite3"
"github.com/statping/statping/database"
"github.com/statping/statping/types"
"github.com/statping/statping/utils"
"time"
)
var SampleHits = 99900.
func Samples() error {
tx := db.Begin()
sg := new(sync.WaitGroup)
for i := int64(1); i <= 5; i++ {
err := createHitsAt(tx, i, sg)
if err != nil {
tx := db.Begin()
tx = createHitsAt(tx, i)
if err := tx.Commit().Error(); err != nil {
log.Error(err)
return err
}
tx = db.Begin()
}
return tx.Error()
return nil
}
func createHitsAt(db database.Database, serviceID int64, sg *sync.WaitGroup) error {
log.Infoln(fmt.Sprintf("Adding sample hit records to service #%d", serviceID))
func createHitsAt(db database.Database, serviceID int64) database.Database {
log.Infoln(fmt.Sprintf("Adding Sample records to service #%d...", serviceID))
createdAt := utils.Now().Add(-3 * types.Day)
p := utils.NewPerlin(2, 2, 5, utils.Now().UnixNano())
i := 0
for hi := 0.; hi <= SampleHits; hi++ {
latency := p.Noise1D(hi / 500)
createdAt = createdAt.Add(30 * time.Second)
hit := &Hit{
Service: serviceID,
Latency: int64(latency * 10000000),
@ -50,15 +44,12 @@ func createHitsAt(db database.Database, serviceID int64, sg *sync.WaitGroup) err
}
db = db.Create(&hit)
if err := db.Error(); err != nil {
return err
}
i++
if createdAt.After(utils.Now()) {
break
}
createdAt = createdAt.Add(30 * time.Second)
}
return db.Commit().Error()
return db
}

View File

@ -1,6 +1,9 @@
package messages
import "github.com/statping/statping/database"
import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/errors"
)
var db database.Database
@ -11,6 +14,9 @@ func SetDB(database database.Database) {
func Find(id int64) (*Message, error) {
var message Message
q := db.Where("id = ?", id).Find(&message)
if q.Error() != nil {
return nil, errors.Missing(message, id)
}
return &message, q.Error()
}

View File

@ -23,7 +23,7 @@ type Notification struct {
Var2 string `gorm:"not null;column:var2" json:"var2,omitempty"`
ApiKey string `gorm:"not null;column:api_key" json:"api_key,omitempty"`
ApiSecret string `gorm:"not null;column:api_secret" json:"api_secret,omitempty"`
Enabled null.NullBool `gorm:"column:enabled;type:boolean;default:false" json:"enabled"`
Enabled null.NullBool `gorm:"column:enabled;type:boolean;default:false" json:"enabled,omitempty"`
Limits int `gorm:"not null;column:limits" json:"limits"`
Removable bool `gorm:"column:removable" json:"removable"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`

View File

@ -1,9 +1,9 @@
package services
import (
"errors"
"fmt"
"github.com/statping/statping/database"
"github.com/statping/statping/types/errors"
"github.com/statping/statping/utils"
"sort"
)
@ -20,7 +20,7 @@ func SetDB(database database.Database) {
func Find(id int64) (*Service, error) {
srv := allServices[id]
if srv == nil {
return nil, errors.New("service not found")
return nil, errors.Missing(&Service{}, id)
}
return srv, nil
}

View File

@ -19,7 +19,7 @@ func findServiceByHash(hash string) *Service {
}
func ServicesFromEnvFile() error {
servicesEnv := utils.Getenv("SERVICES_FILE", "").(string)
servicesEnv := utils.Params.GetString("SERVICES_FILE")
if servicesEnv == "" {
return nil
}

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