Merge branch 'main' into encryption/use-secrets-service

This commit is contained in:
Tania B
2021-10-22 13:10:12 +03:00
113 changed files with 3187 additions and 2343 deletions
+241 -277
View File
@@ -96,6 +96,36 @@ steps:
TEST_MAX_WORKERS: 50%
image: grafana/build-container:1.4.3
name: test-frontend
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--variants linux-x64,linux-x64-musl,osx64,win64,armv6 --no-pull-enterprise
@@ -225,38 +255,6 @@ steps:
dry_run: true
edition: oss
ubuntu: false
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
trigger:
event:
- pull_request
@@ -370,6 +368,36 @@ steps:
TEST_MAX_WORKERS: 50%
image: grafana/build-container:1.4.3
name: test-frontend
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
@@ -540,38 +568,6 @@ steps:
ubuntu: true
username:
from_secret: docker_user
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./scripts/circle-release-canary-packages.sh
depends_on:
@@ -585,8 +581,6 @@ steps:
- ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads
depends_on:
- end-to-end-tests
- mysql-integration-tests
- postgres-integration-tests
environment:
GCP_GRAFANA_UPLOAD_KEY:
from_secret: gcp_key
@@ -827,6 +821,36 @@ steps:
TEST_MAX_WORKERS: 50%
image: grafana/build-container:1.4.3
name: test-frontend
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --github-token $${GITHUB_TOKEN}
--no-pull-enterprise ${DRONE_TAG}
@@ -949,38 +973,6 @@ steps:
ubuntu: true
username:
from_secret: docker_user
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- yarn storybook:build
- ./bin/grabpl verify-storybook
@@ -1003,8 +995,6 @@ steps:
- ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads
depends_on:
- end-to-end-tests
- mysql-integration-tests
- postgres-integration-tests
environment:
GCP_GRAFANA_UPLOAD_KEY:
from_secret: gcp_key
@@ -1209,6 +1199,36 @@ steps:
TEST_MAX_WORKERS: 50%
image: grafana/build-container:1.4.3
name: test-frontend
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN}
--no-pull-enterprise ${DRONE_TAG}
@@ -1367,38 +1387,6 @@ steps:
ubuntu: true
username:
from_secret: docker_user
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- dockerize -wait tcp://redis:6379/0 -timeout 120s
- ./bin/grabpl integration-tests
@@ -1432,8 +1420,6 @@ steps:
- ./bin/grabpl upload-packages --edition enterprise --packages-bucket grafana-downloads
depends_on:
- end-to-end-tests
- mysql-integration-tests
- postgres-integration-tests
- redis-integration-tests
- memcached-integration-tests
environment:
@@ -1496,8 +1482,6 @@ steps:
- ./bin/grabpl upload-packages --edition enterprise2 --packages-bucket grafana-downloads-enterprise2
depends_on:
- end-to-end-tests-enterprise2
- mysql-integration-tests
- postgres-integration-tests
- redis-integration-tests
- memcached-integration-tests
environment:
@@ -1771,6 +1755,36 @@ steps:
TEST_MAX_WORKERS: 50%
image: grafana/build-container:1.4.3
name: test-frontend
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --github-token $${GITHUB_TOKEN}
--no-pull-enterprise v7.3.0-test
@@ -1885,38 +1899,6 @@ steps:
dry_run: true
edition: oss
ubuntu: true
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- yarn storybook:build
- ./bin/grabpl verify-storybook
@@ -1939,8 +1921,6 @@ steps:
- ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads-test
depends_on:
- end-to-end-tests
- mysql-integration-tests
- postgres-integration-tests
environment:
GCP_GRAFANA_UPLOAD_KEY:
from_secret: gcp_key
@@ -2142,6 +2122,36 @@ steps:
TEST_MAX_WORKERS: 50%
image: grafana/build-container:1.4.3
name: test-frontend
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition enterprise --github-token $${GITHUB_TOKEN}
--no-pull-enterprise v7.3.0-test
@@ -2292,38 +2302,6 @@ steps:
dry_run: true
edition: enterprise
ubuntu: true
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- dockerize -wait tcp://redis:6379/0 -timeout 120s
- ./bin/grabpl integration-tests
@@ -2357,8 +2335,6 @@ steps:
- ./bin/grabpl upload-packages --edition enterprise --packages-bucket grafana-downloads-test
depends_on:
- end-to-end-tests
- mysql-integration-tests
- postgres-integration-tests
- redis-integration-tests
- memcached-integration-tests
environment:
@@ -2421,8 +2397,6 @@ steps:
- ./bin/grabpl upload-packages --edition enterprise2 --packages-bucket grafana-downloads-test
depends_on:
- end-to-end-tests-enterprise2
- mysql-integration-tests
- postgres-integration-tests
- redis-integration-tests
- memcached-integration-tests
environment:
@@ -2701,6 +2675,36 @@ steps:
TEST_MAX_WORKERS: 50%
image: grafana/build-container:1.4.3
name: test-frontend
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition oss --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
@@ -2813,38 +2817,6 @@ steps:
dry_run: true
edition: oss
ubuntu: true
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- yarn storybook:build
- ./bin/grabpl verify-storybook
@@ -2867,8 +2839,6 @@ steps:
- ./bin/grabpl upload-packages --edition oss --packages-bucket grafana-downloads
depends_on:
- end-to-end-tests
- mysql-integration-tests
- postgres-integration-tests
environment:
GCP_GRAFANA_UPLOAD_KEY:
from_secret: gcp_key
@@ -3044,6 +3014,36 @@ steps:
TEST_MAX_WORKERS: 50%
image: grafana/build-container:1.4.3
name: test-frontend
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- initialize
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- ./bin/grabpl build-backend --jobs 8 --edition enterprise --build-id ${DRONE_BUILD_NUMBER}
--no-pull-enterprise
@@ -3190,38 +3190,6 @@ steps:
dry_run: true
edition: enterprise
ubuntu: true
- commands:
- apt-get update
- apt-get install -yq postgresql-client
- dockerize -wait tcp://postgres:5432 -timeout 120s
- psql -p 5432 -h postgres -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql
- go clean -testcache
- ./bin/grabpl integration-tests --database postgres
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: grafana/build-container:1.4.3
name: postgres-integration-tests
- commands:
- apt-get update
- apt-get install -yq default-mysql-client
- dockerize -wait tcp://mysql:3306 -timeout 120s
- cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h mysql -P 3306 -u root
-prootpass
- go clean -testcache
- ./bin/grabpl integration-tests --database mysql
depends_on:
- test-backend
- test-frontend
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql
image: grafana/build-container:1.4.3
name: mysql-integration-tests
- commands:
- yarn storybook:build
- ./bin/grabpl verify-storybook
@@ -3264,8 +3232,6 @@ steps:
- ./bin/grabpl upload-packages --edition enterprise --packages-bucket grafana-downloads
depends_on:
- end-to-end-tests
- mysql-integration-tests
- postgres-integration-tests
- redis-integration-tests
- memcached-integration-tests
environment:
@@ -3328,8 +3294,6 @@ steps:
- ./bin/grabpl upload-packages --edition enterprise2 --packages-bucket grafana-downloads-enterprise2
depends_on:
- end-to-end-tests-enterprise2
- mysql-integration-tests
- postgres-integration-tests
- redis-integration-tests
- memcached-integration-tests
environment:
@@ -3485,6 +3449,6 @@ kind: secret
name: drone_token
---
kind: signature
hmac: 0d26eda19dc6e530d02c734704c8a9d01beb5082c7e14b2609577b8695ed06f5
hmac: 103fe906b04e8a336e3113007921d62a57107bc7a86a14442b438fa54406c084
...
+20 -12
View File
@@ -14,13 +14,15 @@
# Documentation owner: Jita Chatterjee
/docs/ @grafana/docs-squad @pkolyvas
/contribute/ @marcusolsson @grafana/docs-squad @pkolyvas
/docs/sources/developers/plugins/ @marcusolsson @grafana/docs-squad
/docs/sources/developers/plugins/ @marcusolsson @grafana/docs-squad @grafana/plugins-platform
/docs/sources/developers/plugins/backend @marcusolsson @grafana/docs-squad @grafana/plugins-platform-backend
/docs/sources/enterprise/ @osg-grafana @grafana/docs-squad
# Backend code
*.go @grafana/backend-platform
go.mod @grafana/backend-platform
go.sum @grafana/backend-platform
/.bingo @grafana/backend-platform
# Continuous Integration
.drone.yml @grafana/grafana-release-eng
@@ -56,25 +58,31 @@ go.sum @grafana/backend-platform
/pkg/services/libraryelements @grafana/user-essentials
/pkg/services/librarypanels @grafana/user-essentials
# Plugins
/pkg/api/pluginproxy @grafana/plugins-platform-backend
/pkg/plugins @grafana/plugins-platform-backend
/pkg/services/datasourceproxy @grafana/plugins-platform-backend
/pkg/services/datasources @grafana/plugins-platform-backend
# Backend code docs
/contribute/style-guides/backend.md @grafana/backend-platform
/contribute/architecture/backend @grafana/backend-platform
/contribute/engineering/backend @grafana/backend-platform
/e2e @grafana/user-essentials
/packages @grafana/user-essentials @grafana/plugins-platform @grafana/grafana-bi-squad
/packages @grafana/user-essentials @grafana/plugins-platform-frontend @grafana/grafana-bi-squad
/packages/grafana-e2e-selectors @grafana/user-essentials
/packages/grafana-e2e @grafana/user-essentials
/packages/grafana-toolkit @grafana/plugins-platform
/packages/grafana-ui/.storybook @grafana/plugins-platform
/packages/grafana-toolkit @grafana/plugins-platform-frontend
/packages/grafana-ui/.storybook @grafana/plugins-platform-frontend
/packages/grafana-ui/src/components/DateTimePickers @grafana/grafana-bi-squad
/packages/grafana-ui/src/components/GraphNG @grafana/grafana-bi-squad
/packages/grafana-ui/src/components/Table @grafana/grafana-bi-squad
/packages/grafana-ui/src/components/TimeSeries @grafana/grafana-bi-squad
/packages/grafana-ui/src/components/uPlot @grafana/grafana-bi-squad
/packages/grafana-ui/src/utils/storybook @grafana/plugins-platform
/packages/grafana-ui/src/utils/storybook @grafana/plugins-platform-frontend
/packages/jaeger-ui-components/ @grafana/observability-squad
/plugins-bundled @grafana/plugins-platform
/plugins-bundled @grafana/plugins-platform-frontend
/public @grafana/user-essentials
/public/app/core/components/TimePicker @grafana/grafana-bi-squad
/public/app/features/canvas/ @grafana/grafana-edge-squad
@@ -93,11 +101,11 @@ go.sum @grafana/backend-platform
/public/app/plugins/panel/geomap @grafana/grafana-edge-squad
/public/app/plugins/panel/canvas @grafana/grafana-edge-squad
/public/app/plugins/panel/icon @grafana/grafana-edge-squad
/scripts/build/release-packages.sh @grafana/plugins-platform
/scripts/circle-release-next-packages.sh @grafana/plugins-platform
/scripts/ci-frontend-metrics.sh @grafana/user-essentials @grafana/plugins-platform @grafana/grafana-bi-squad
/scripts/ci-reference-docs-build.sh @grafana/plugins-platform
/scripts/ci-reference-docs-lint.sh @grafana/plugins-platform
/scripts/build/release-packages.sh @grafana/plugins-platform-frontend
/scripts/circle-release-next-packages.sh @grafana/plugins-platform-frontend
/scripts/ci-frontend-metrics.sh @grafana/user-essentials @grafana/plugins-platform-frontend @grafana/grafana-bi-squad
/scripts/ci-reference-docs-build.sh @grafana/plugins-platform-frontend
/scripts/ci-reference-docs-lint.sh @grafana/plugins-platform-frontend
/scripts/grunt @grafana/frontend-ops
/scripts/webpack @grafana/frontend-ops
/scripts/generate-a11y-report.sh @grafana/user-essentials
@@ -111,7 +119,7 @@ lerna.json @grafana/frontend-ops
.pa11yci-pr.conf.js @grafana/user-essentials
# @grafana/ui component documentation
*.mdx @marcusolsson @jessover9000 @grafana/plugins-platform
*.mdx @marcusolsson @jessover9000 @grafana/plugins-platform-frontend
# Core datasources
/public/app/plugins/datasource/cloudwatch @grafana/cloud-datasources @grafana/observability-squad
+1 -1
View File
@@ -153,7 +153,7 @@ clean: ## Clean up intermediate build artifacts.
# This repository's configuration is protected (https://readme.drone.io/signature/).
# Use this make target to regenerate the configuration YAML files when
# you modify starlark files.
drone:
drone: $(DRONE)
@if [ "$(DRONE_VERSION)" != "1.4.0" ]; then\
echo "WARN: You are using drone-cli ${DRONE_VERSION}. Please update your LOCAL version to 1.4.0. Using latest bingo version...";\
fi
+1 -1
View File
@@ -11,7 +11,7 @@ weight = 300
The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP
group memberships and Grafana Organization user roles.
> [Enhanced LDAP authentication]({{< relref "../enterprise/enhanced_ldap.md" >}}) is available in [Grafana Cloud Pro and Advanced](https://grafana.com/docs/grafana-cloud/) and in [Grafana Enterprise]({{< relref "../enterprise" >}}).
> [Enhanced LDAP authentication]({{< relref "../enterprise/enhanced_ldap.md" >}}) is available in [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/) and in [Grafana Enterprise]({{< relref "../enterprise" >}}).
> Refer to [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to understand how you can control access with fine-grained permissions.
+1 -1
View File
@@ -9,7 +9,7 @@ weight = 600
The enhanced LDAP integration adds additional functionality on top of the [LDAP integration]({{< relref "../auth/ldap.md" >}}) available in the open source edition of Grafana.
> Enhanced LDAP integration is only available in Grafana Enterprise.
> Enhanced LDAP integration is only available in [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/) and in [Grafana Enterprise]({{< relref "../enterprise" >}}).
> Refer to [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to understand how you can control access with fine-grained permissions.
+2 -1
View File
@@ -91,7 +91,8 @@ margin-bottom: 25px;
.footer {
background-color: #2e2e2e;
color: #999999;
margin-top: 20px;
margin: 0 auto;
width: 100%;
}
@media only screen and (max-width: 600px) {
+21 -6
View File
@@ -5,21 +5,21 @@
<tr>
<td colspan="2" class="annotations">
[[ range .Annotations.SortedPairs ]]
<p>[[ .Name ]]: [[ .Value ]]</p>
<p><span class="annotations-heading">[[ .Name ]]:</span> <span class="annotations-value">[[ .Value ]]</span></p>
[[ end ]]
</td>
</tr>
[[ end ]]
<tr>
<td valign="top" class="labels-heading">Labels:</td>
<td>
<td colspan="2">
<span class="labels-heading">Labels:</span>
<ul class="labels-list">
[[ range .Labels.SortedPairs ]]<li>[[ .Name ]]: [[ .Value ]]</li>[[ end ]]
</ul>
</td>
</tr>
<tr>
<td colspan="2">
<td colspan="2" class="actions">
[[ if .SilenceURL ]]
<a
href="[[ .SilenceURL ]]"
@@ -116,10 +116,24 @@
font-size: 14px;
font-weight: bold;
vertical-align: top;
display: inline-block;
}
.annotations-heading {
font-weight: bold;
text-transform: capitalize;
}
.annotations-value {
padding-left: 8px;
}
.labels-list {
font-size: 14px;
vertical-align: top;
margin: 0;
display: inline-block;
padding-left: 8px;
}
.actions {
padding: 24px 0 12px 0;
}
.section-heading {
color: #2c3235;
@@ -132,6 +146,7 @@
font-weight: bold;
padding: 0 0 0 12px;
text-decoration: underline;
vertical-align: middle;
}
.status-tag {
color: #ffffff;
@@ -213,6 +228,6 @@
</table>
</table>
</td>
</tr>
</tr>
</table>
[[ end ]]
[[ end ]]
+1
View File
@@ -186,6 +186,7 @@ require (
github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f // indirect
github.com/grafana/grafana-google-sdk-go v0.0.0-20211019132340-3ff525a010d5
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.0 // indirect
+2
View File
@@ -1204,6 +1204,8 @@ github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5J
github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/grafana/grafana-aws-sdk v0.7.0 h1:D+Lhxi3P/7vpyDHUK/fdX9bL2mRz8hLG04ucNf1E02o=
github.com/grafana/grafana-aws-sdk v0.7.0/go.mod h1:+pPo5U+pX0zWimR7YBc7ASeSQfbRkcTyQYqMiAj7G5U=
github.com/grafana/grafana-google-sdk-go v0.0.0-20211019132340-3ff525a010d5 h1:o7w/t0nLNfkERMdj09U0h3Fl63z8ws1CxwiImeUKLIk=
github.com/grafana/grafana-google-sdk-go v0.0.0-20211019132340-3ff525a010d5/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
github.com/grafana/grafana-plugin-sdk-go v0.79.0/go.mod h1:NvxLzGkVhnoBKwzkst6CFfpMFKwAdIUZ1q8ssuLeF60=
github.com/grafana/grafana-plugin-sdk-go v0.114.0 h1:9I55IXw7mOT71tZ/pdqCaWGz8vxfz31CXjaDtBV9ZBo=
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
+2 -2
View File
@@ -1,4 +1,4 @@
{
"stable": "8.2.1",
"testing": "8.2.1"
"stable": "8.2.2",
"testing": "8.2.2"
}
+1 -1
View File
@@ -113,7 +113,7 @@
"@types/logfmt": "^1.2.1",
"@types/lru-cache": "^5.1.0",
"@types/mousetrap": "1.6.3",
"@types/node": "13.7.0",
"@types/node": "14.17.27",
"@types/papaparse": "5.2.0",
"@types/pluralize": "^0.0.29",
"@types/prismjs": "1.16.0",
+1 -1
View File
@@ -42,7 +42,7 @@
"@types/jquery": "3.3.38",
"@types/lodash": "4.14.123",
"@types/marked": "1.1.0",
"@types/node": "10.14.1",
"@types/node": "14.17.27",
"@types/papaparse": "5.2.0",
"@types/react": "17.0.30",
"@types/rollup-plugin-visualizer": "4.2.1",
@@ -53,6 +53,7 @@ export interface FeatureToggles {
prometheusMonaco: boolean;
newNavigation: boolean;
fullRangeLogsVolume: boolean;
autoLoadFullRangeLogsVolume: boolean;
}
/**
+1 -1
View File
@@ -26,7 +26,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "16.0.0",
"@rollup/plugin-node-resolve": "10.0.0",
"@types/node": "13.7.7",
"@types/node": "14.17.27",
"@types/rollup-plugin-visualizer": "4.2.1",
"@types/systemjs": "^0.20.6",
"rollup": "2.33.3",
+1 -1
View File
@@ -32,7 +32,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "16.0.0",
"@rollup/plugin-node-resolve": "10.0.0",
"@types/node": "13.7.7",
"@types/node": "14.17.27",
"@types/rollup-plugin-visualizer": "4.2.1",
"date-fns": "2.21.3",
"rollup": "2.33.3",
+1
View File
@@ -69,6 +69,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
prometheusMonaco: false,
newNavigation: false,
fullRangeLogsVolume: false,
autoLoadFullRangeLogsVolume: false,
};
licenseInfo: LicenseInfo = {} as LicenseInfo;
rendererAvailable = false;
+1 -1
View File
@@ -37,7 +37,7 @@
"@types/fs-extra": "^9.0.13",
"@types/inquirer": "^6.0.3",
"@types/jest": "26.0.15",
"@types/node": "^12.0.4",
"@types/node": "^14.0.0",
"@types/prettier": "^2.4.0",
"@types/react-dev-utils": "^9.0.4",
"@types/rimraf": "^3.0.0",
+1 -1
View File
@@ -100,7 +100,7 @@
"@types/jquery": "3.3.38",
"@types/lodash": "4.14.123",
"@types/mock-raf": "1.0.2",
"@types/node": "10.14.1",
"@types/node": "14.17.27",
"@types/papaparse": "5.2.0",
"@types/react": "17.0.30",
"@types/react-beautiful-dnd": "13.1.1",
@@ -33,4 +33,39 @@ describe('ErrorBoundary', () => {
expect(context.contexts.react).toHaveProperty('componentStack');
expect(context.contexts.react.componentStack).toMatch(/^\s+at ErrorThrower (.*)\s+at ErrorBoundary (.*)\s*$/);
});
it('should recover when when recover props change', async () => {
const problem = new Error('things went terribly wrong');
let renderCount = 0;
const { rerender } = render(
<ErrorBoundary dependencies={[1, 2]}>
{({ error }) => {
if (!error) {
renderCount += 1;
return <ErrorThrower error={problem} />;
} else {
return <p>{error.message}</p>;
}
}}
</ErrorBoundary>
);
await screen.findByText(problem.message);
rerender(
<ErrorBoundary dependencies={[1, 3]}>
{({ error }) => {
if (!error) {
renderCount += 1;
return <ErrorThrower error={problem} />;
} else {
return <p>{error.message}</p>;
}
}}
</ErrorBoundary>
);
expect(renderCount).toBe(2);
});
});
@@ -14,6 +14,12 @@ export interface ErrorBoundaryApi {
interface Props {
children: (r: ErrorBoundaryApi) => ReactNode;
/** Will re-render children after error if recover values changes */
dependencies?: any[];
/** Callback called on error */
onError?: (error: Error) => void;
/** Callback error state is cleared due to recover props change */
onRecover?: () => void;
}
interface State {
@@ -29,10 +35,29 @@ export class ErrorBoundary extends PureComponent<Props, State> {
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack } } });
this.setState({
error: error,
errorInfo: errorInfo,
});
this.setState({ error, errorInfo });
if (this.props.onError) {
this.props.onError(error);
}
}
componentDidUpdate(prevProps: Props) {
const { dependencies, onRecover } = this.props;
if (this.state.error) {
if (dependencies && prevProps.dependencies) {
for (let i = 0; i < dependencies.length; i++) {
if (dependencies[i] !== prevProps.dependencies[i]) {
this.setState({ error: null, errorInfo: null });
if (onRecover) {
onRecover();
}
break;
}
}
}
}
}
render() {
@@ -60,6 +85,9 @@ export interface ErrorBoundaryAlertProps {
/** 'page' will render full page error with stacktrace. 'alertbox' will render an <Alert />. Default 'alertbox' */
style?: 'page' | 'alertbox';
/** Will re-render children after error if recover values changes */
dependencies?: any[];
}
export class ErrorBoundaryAlert extends PureComponent<ErrorBoundaryAlertProps> {
@@ -69,10 +97,10 @@ export class ErrorBoundaryAlert extends PureComponent<ErrorBoundaryAlertProps> {
};
render() {
const { title, children, style } = this.props;
const { title, children, style, dependencies } = this.props;
return (
<ErrorBoundary>
<ErrorBoundary dependencies={dependencies}>
{({ error, errorInfo }) => {
if (!errorInfo) {
return children;
@@ -88,6 +88,7 @@ const CustomControl = (props: any) => {
export function SelectBase<T>({
allowCustomValue = false,
allowCreateWhileLoading = false,
'aria-label': ariaLabel,
autoFocus = false,
backspaceRemovesValue = true,
@@ -227,6 +228,7 @@ export function SelectBase<T>({
if (allowCustomValue) {
ReactSelectComponent = Creatable as any;
creatableProps.allowCreateWhileLoading = allowCreateWhileLoading;
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
creatableProps.onCreateOption = onCreateOption;
creatableProps.isValidNewOption = isValidNewOption;
@@ -9,6 +9,7 @@ export type InputActionMeta = {
export interface SelectCommonProps<T> {
/** Aria label applied to the input field */
['aria-label']?: string;
allowCreateWhileLoading?: boolean;
allowCustomValue?: boolean;
/** Focus is set to the Select when rendered*/
autoFocus?: boolean;
@@ -92,6 +92,15 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
getYRange(field: Field) {
let { min, max } = this.state.alignedDataFrame.fields[1].state?.range!;
if (min === max) {
if (min === 0) {
max = 100;
} else {
min = 0;
max! *= 2;
}
}
return [
Math.max(min!, field.config.min ?? -Infinity),
Math.min(max!, field.config.max ?? Infinity),
+7 -7
View File
@@ -303,11 +303,11 @@ func (hs *HTTPServer) registerRoutes() {
}, reqOrgAdmin)
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
apiRoute.Any("/datasources/:id/resources", hs.CallDatasourceResource)
apiRoute.Any("/datasources/:id/resources/*", hs.CallDatasourceResource)
apiRoute.Any("/datasources/:id/health", routing.Wrap(hs.CheckDatasourceHealth))
apiRoute.Any("/datasources/proxy/:id/*", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), hs.ProxyDataSourceRequest)
apiRoute.Any("/datasources/proxy/:id", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), hs.ProxyDataSourceRequest)
apiRoute.Any("/datasources/:id/resources", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), hs.CallDatasourceResource)
apiRoute.Any("/datasources/:id/resources/*", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), hs.CallDatasourceResource)
apiRoute.Any("/datasources/:id/health", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), routing.Wrap(hs.CheckDatasourceHealth))
// Folders
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
@@ -373,10 +373,10 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Get("/search/", routing.Wrap(Search))
// metrics
apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), routing.Wrap(hs.QueryMetrics))
apiRoute.Post("/tsdb/query", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), bind(dtos.MetricRequest{}), routing.Wrap(hs.QueryMetrics))
// DataSource w/ expressions
apiRoute.Post("/ds/query", bind(dtos.MetricRequest{}), routing.Wrap(hs.QueryMetricsV2))
apiRoute.Post("/ds/query", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), bind(dtos.MetricRequest{}), routing.Wrap(hs.QueryMetricsV2))
apiRoute.Group("/alerts", func(alertsRoute routing.RouteRegister) {
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), routing.Wrap(hs.AlertTest))
+1 -2
View File
@@ -105,8 +105,7 @@ func getTokenProvider(ctx context.Context, cfg *setting.Cfg, ds DSInfo, pluginRo
if jwtTokenAuth == nil {
return nil, fmt.Errorf("'jwtTokenAuth' not configured for authentication type '%s'", authType)
}
provider := newGceAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth)
return provider, nil
return newGceAccessTokenProvider(ctx, ds, pluginRoute, jwtTokenAuth), nil
case "jwt":
if jwtTokenAuth == nil {
+13 -25
View File
@@ -2,43 +2,31 @@ package pluginproxy
import (
"context"
"time"
googletokenprovider "github.com/grafana/grafana-google-sdk-go/pkg/tokenprovider"
"github.com/grafana/grafana/pkg/plugins"
"golang.org/x/oauth2/google"
)
type gceAccessTokenProvider struct {
datasourceId int64
datasourceUpdated time.Time
ctx context.Context
route *plugins.AppPluginRoute
authParams *plugins.JwtTokenAuth
source googletokenprovider.TokenProvider
ctx context.Context
}
func newGceAccessTokenProvider(ctx context.Context, ds DSInfo, pluginRoute *plugins.AppPluginRoute,
authParams *plugins.JwtTokenAuth) *gceAccessTokenProvider {
cfg := googletokenprovider.Config{
RoutePath: pluginRoute.Path,
RouteMethod: pluginRoute.Method,
DataSourceID: ds.ID,
DataSourceUpdated: ds.Updated,
Scopes: authParams.Scopes,
}
return &gceAccessTokenProvider{
datasourceId: ds.ID,
datasourceUpdated: ds.Updated,
ctx: ctx,
route: pluginRoute,
authParams: authParams,
source: googletokenprovider.NewGceAccessTokenProvider(cfg),
ctx: ctx,
}
}
func (provider *gceAccessTokenProvider) GetAccessToken() (string, error) {
tokenSrc, err := google.DefaultTokenSource(provider.ctx, provider.authParams.Scopes...)
if err != nil {
logger.Error("Failed to get default token from meta data server", "error", err)
return "", err
} else {
token, err := tokenSrc.Token()
if err != nil {
logger.Error("Failed to get default access token from meta data server", "error", err)
return "", err
} else {
return token.AccessToken, nil
}
}
return provider.source.GetAccessToken(provider.ctx)
}
+28 -77
View File
@@ -2,95 +2,46 @@ package pluginproxy
import (
"context"
"fmt"
"sync"
"time"
googletokenprovider "github.com/grafana/grafana-google-sdk-go/pkg/tokenprovider"
"github.com/grafana/grafana/pkg/plugins"
"golang.org/x/oauth2"
"golang.org/x/oauth2/jwt"
)
var (
oauthJwtTokenCache = oauthJwtTokenCacheType{
cache: map[string]*oauth2.Token{},
}
)
type oauthJwtTokenCacheType struct {
cache map[string]*oauth2.Token
sync.Mutex
}
type jwtAccessTokenProvider struct {
datasourceId int64
datasourceUpdated time.Time
ctx context.Context
route *plugins.AppPluginRoute
authParams *plugins.JwtTokenAuth
source googletokenprovider.TokenProvider
ctx context.Context
}
func newJwtAccessTokenProvider(ctx context.Context, ds DSInfo, pluginRoute *plugins.AppPluginRoute,
authParams *plugins.JwtTokenAuth) *jwtAccessTokenProvider {
jwtConf := &googletokenprovider.JwtTokenConfig{}
if val, ok := authParams.Params["client_email"]; ok {
jwtConf.Email = val
}
if val, ok := authParams.Params["private_key"]; ok {
jwtConf.PrivateKey = []byte(val)
}
if val, ok := authParams.Params["token_uri"]; ok {
jwtConf.URI = val
}
cfg := googletokenprovider.Config{
RoutePath: pluginRoute.Path,
RouteMethod: pluginRoute.Method,
DataSourceID: ds.ID,
DataSourceUpdated: ds.Updated,
Scopes: authParams.Scopes,
JwtTokenConfig: jwtConf,
}
return &jwtAccessTokenProvider{
datasourceId: ds.ID,
datasourceUpdated: ds.Updated,
ctx: ctx,
route: pluginRoute,
authParams: authParams,
source: googletokenprovider.NewJwtAccessTokenProvider(cfg),
ctx: ctx,
}
}
func (provider *jwtAccessTokenProvider) GetAccessToken() (string, error) {
oauthJwtTokenCache.Lock()
defer oauthJwtTokenCache.Unlock()
if cachedToken, found := oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()]; found {
if cachedToken.Expiry.After(timeNow().Add(time.Second * 10)) {
logger.Debug("Using token from cache")
return cachedToken.AccessToken, nil
}
}
conf := &jwt.Config{}
if val, ok := provider.authParams.Params["client_email"]; ok {
conf.Email = val
}
if val, ok := provider.authParams.Params["private_key"]; ok {
conf.PrivateKey = []byte(val)
}
if val, ok := provider.authParams.Params["token_uri"]; ok {
conf.TokenURL = val
}
conf.Scopes = provider.authParams.Scopes
token, err := getTokenSource(conf, provider.ctx)
if err != nil {
return "", err
}
oauthJwtTokenCache.cache[provider.getAccessTokenCacheKey()] = token
logger.Info("Got new access token", "ExpiresOn", token.Expiry)
return token.AccessToken, nil
}
// getTokenSource gets a token source.
// Stubbable by tests.
var getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
tokenSrc := conf.TokenSource(ctx)
token, err := tokenSrc.Token()
if err != nil {
return nil, err
}
return token, nil
}
func (provider *jwtAccessTokenProvider) getAccessTokenCacheKey() string {
return fmt.Sprintf("%v_%v_%v_%v", provider.datasourceId, provider.datasourceUpdated.Unix(), provider.route.Path, provider.route.Method)
return provider.source.GetAccessToken(provider.ctx)
}
@@ -1,7 +1,6 @@
package pluginproxy
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -13,105 +12,12 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins"
"golang.org/x/oauth2"
"golang.org/x/oauth2/jwt"
)
var (
token map[string]interface{}
)
func TestAccessToken_pluginWithJWTTokenAuthRoute(t *testing.T) {
pluginRoute := &plugins.AppPluginRoute{
Path: "pathwithjwttoken1",
URL: "https://api.jwt.io/some/path",
Method: "GET",
JwtTokenAuth: &plugins.JwtTokenAuth{
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
Scopes: []string{
"https://www.testapi.com/auth/monitoring.read",
"https://www.testapi.com/auth/cloudplatformprojects.readonly",
},
Params: map[string]string{
"token_uri": "{{.JsonData.tokenUri}}",
"client_email": "{{.JsonData.clientEmail}}",
"private_key": "{{.SecureJsonData.privateKey}}",
},
},
}
authParams := &plugins.JwtTokenAuth{
Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
Scopes: []string{
"https://www.testapi.com/auth/monitoring.read",
"https://www.testapi.com/auth/cloudplatformprojects.readonly",
},
Params: map[string]string{
"token_uri": "login.url.com/token",
"client_email": "test@test.com",
"private_key": "testkey",
},
}
setUp := func(t *testing.T, fn func(*jwt.Config, context.Context) (*oauth2.Token, error)) {
origFn := getTokenSource
t.Cleanup(func() {
getTokenSource = origFn
})
getTokenSource = fn
}
ds := DSInfo{ID: 1, Updated: time.Now()}
t.Run("should fetch token using JWT private key", func(t *testing.T) {
setUp(t, func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
return &oauth2.Token{AccessToken: "abc"}, nil
})
provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams)
token, err := provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, "abc", token)
})
t.Run("should set JWT config values", func(t *testing.T) {
setUp(t, func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
assert.Equal(t, "test@test.com", conf.Email)
assert.Equal(t, []byte("testkey"), conf.PrivateKey)
assert.Equal(t, 2, len(conf.Scopes))
assert.Equal(t, "https://www.testapi.com/auth/monitoring.read", conf.Scopes[0])
assert.Equal(t, "https://www.testapi.com/auth/cloudplatformprojects.readonly", conf.Scopes[1])
assert.Equal(t, "login.url.com/token", conf.TokenURL)
return &oauth2.Token{AccessToken: "abc"}, nil
})
provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams)
_, err := provider.GetAccessToken()
require.NoError(t, err)
})
t.Run("should use cached token on second call", func(t *testing.T) {
setUp(t, func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: "abc",
Expiry: time.Now().Add(1 * time.Minute)}, nil
})
provider := newJwtAccessTokenProvider(context.Background(), ds, pluginRoute, authParams)
token1, err := provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, "abc", token1)
getTokenSource = func(conf *jwt.Config, ctx context.Context) (*oauth2.Token, error) {
return &oauth2.Token{AccessToken: "error: cache not used"}, nil
}
token2, err := provider.GetAccessToken()
require.NoError(t, err)
assert.Equal(t, "abc", token2)
})
}
func TestAccessToken_pluginWithTokenAuthRoute(t *testing.T) {
apiHandler := http.NewServeMux()
server := httptest.NewServer(apiHandler)
+19 -1
View File
@@ -10,6 +10,7 @@ const (
ActionProvisioningReload = "provisioning:reload"
ActionDatasourcesRead = "datasources:read"
ActionDatasourcesQuery = "datasources:query"
ActionDatasourcesCreate = "datasources:create"
ActionDatasourcesWrite = "datasources:write"
ActionDatasourcesDelete = "datasources:delete"
@@ -63,11 +64,17 @@ func (hs *HTTPServer) declareFixedRoles() error {
Action: ActionDatasourcesWrite,
Scope: ScopeDatasourcesAll,
},
{Action: ActionDatasourcesCreate},
{
Action: ActionDatasourcesCreate,
},
{
Action: ActionDatasourcesDelete,
Scope: ScopeDatasourcesAll,
},
{
Action: ActionDatasourcesQuery,
Scope: ScopeDatasourcesAll,
},
},
},
Grants: []string{string(models.ROLE_ADMIN)},
@@ -86,6 +93,17 @@ func (hs *HTTPServer) declareFixedRoles() error {
},
Grants: []string{string(models.ROLE_VIEWER)},
},
{
Role: accesscontrol.RoleDTO{
Version: 1,
Name: "fixed:datasources:compatibility:querier",
Description: "Query data sources when data source permissions are not in use",
Permissions: []accesscontrol.Permission{
{Action: ActionDatasourcesQuery},
},
},
Grants: []string{string(models.ROLE_VIEWER)},
},
}
return hs.AccessControl.DeclareFixedRoles(registrations...)
+1 -1
View File
@@ -99,7 +99,7 @@ func (kv *kvStoreSQL) Del(ctx context.Context, orgId int64, namespace string, ke
func (kv *kvStoreSQL) Keys(ctx context.Context, orgId int64, namespace string, keyPrefix string) ([]Key, error) {
var keys []Key
err := kv.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
query := dbSession.Where("namespace = ?", namespace).And("key LIKE ?", keyPrefix+"%")
query := dbSession.Where("namespace = ?", namespace).And("\"key\" LIKE ?", keyPrefix+"%")
if orgId != AllOrganizations {
query.And("org_id = ?", orgId)
}
@@ -10,8 +10,9 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/validations"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReplaceIllegalCharswithUnderscore(t *testing.T) {
@@ -80,57 +81,55 @@ func TestWhenAlertManagerShouldNotify(t *testing.T) {
//nolint:goconst
func TestAlertmanagerNotifier(t *testing.T) {
Convey("Alertmanager notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
_, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("from settings", func() {
json := `{ "url": "http://127.0.0.1:9093/", "basicAuthUser": "user", "basicAuthPassword": "password" }`
t.Run("from settings", func(t *testing.T) {
json := `{ "url": "http://127.0.0.1:9093/", "basicAuthUser": "user", "basicAuthPassword": "password" }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
alertmanagerNotifier := not.(*AlertmanagerNotifier)
not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
alertmanagerNotifier := not.(*AlertmanagerNotifier)
So(err, ShouldBeNil)
So(alertmanagerNotifier.BasicAuthUser, ShouldEqual, "user")
So(alertmanagerNotifier.BasicAuthPassword, ShouldEqual, "password")
So(alertmanagerNotifier.URL, ShouldResemble, []string{"http://127.0.0.1:9093/"})
})
require.NoError(t, err)
require.Equal(t, alertmanagerNotifier.BasicAuthUser, "user")
require.Equal(t, alertmanagerNotifier.BasicAuthPassword, "password")
require.Equal(t, alertmanagerNotifier.URL, []string{"http://127.0.0.1:9093/"})
})
Convey("from settings with multiple alertmanager", func() {
json := `{ "url": "http://alertmanager1:9093,http://alertmanager2:9093" }`
t.Run("from settings with multiple alertmanager", func(t *testing.T) {
json := `{ "url": "http://alertmanager1:9093,http://alertmanager2:9093" }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "alertmanager",
Type: "alertmanager",
Settings: settingsJSON,
}
not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
alertmanagerNotifier := not.(*AlertmanagerNotifier)
not, err := NewAlertmanagerNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
alertmanagerNotifier := not.(*AlertmanagerNotifier)
So(err, ShouldBeNil)
So(alertmanagerNotifier.URL, ShouldResemble, []string{"http://alertmanager1:9093", "http://alertmanager2:9093"})
})
require.NoError(t, err)
require.Equal(t, alertmanagerNotifier.URL, []string{"http://alertmanager1:9093", "http://alertmanager2:9093"})
})
})
}
+26 -27
View File
@@ -9,8 +9,9 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/validations"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestShouldSendAlertNotification(t *testing.T) {
@@ -184,38 +185,36 @@ func TestShouldSendAlertNotification(t *testing.T) {
}
func TestBaseNotifier(t *testing.T) {
Convey("default constructor for notifiers", t, func() {
bJSON := simplejson.New()
bJSON := simplejson.New()
model := &models.AlertNotification{
Uid: "1",
Name: "name",
Type: "email",
Settings: bJSON,
}
model := &models.AlertNotification{
Uid: "1",
Name: "name",
Type: "email",
Settings: bJSON,
}
Convey("can parse false value", func() {
bJSON.Set("uploadImage", false)
t.Run("can parse false value", func(t *testing.T) {
bJSON.Set("uploadImage", false)
base := NewNotifierBase(model)
So(base.UploadImage, ShouldBeFalse)
})
base := NewNotifierBase(model)
require.False(t, base.UploadImage)
})
Convey("can parse true value", func() {
bJSON.Set("uploadImage", true)
t.Run("can parse true value", func(t *testing.T) {
bJSON.Set("uploadImage", true)
base := NewNotifierBase(model)
So(base.UploadImage, ShouldBeTrue)
})
base := NewNotifierBase(model)
require.True(t, base.UploadImage)
})
Convey("default value should be true for backwards compatibility", func() {
base := NewNotifierBase(model)
So(base.UploadImage, ShouldBeTrue)
})
t.Run("default value should be true for backwards compatibility", func(t *testing.T) {
base := NewNotifierBase(model)
require.True(t, base.UploadImage)
})
Convey("default value should be false for backwards compatibility", func() {
base := NewNotifierBase(model)
So(base.DisableResolveMessage, ShouldBeFalse)
})
t.Run("default value should be false for backwards compatibility", func(t *testing.T) {
base := NewNotifierBase(model)
require.False(t, base.DisableResolveMessage)
})
}
@@ -9,51 +9,50 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/validations"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestDingDingNotifier(t *testing.T) {
Convey("Dingding notifier tests", t, func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "dingding_testing",
Type: "dingding",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "dingding_testing",
Type: "dingding",
Settings: settingsJSON,
}
_, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
Convey("settings should trigger incident", func() {
json := `{ "url": "https://www.google.com" }`
_, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
t.Run("settings should trigger incident", func(t *testing.T) {
json := `{ "url": "https://www.google.com" }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "dingding_testing",
Type: "dingding",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "dingding_testing",
Type: "dingding",
Settings: settingsJSON,
}
not, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
notifier := not.(*DingDingNotifier)
not, err := newDingDingNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
notifier := not.(*DingDingNotifier)
So(err, ShouldBeNil)
So(notifier.Name, ShouldEqual, "dingding_testing")
So(notifier.Type, ShouldEqual, "dingding")
So(notifier.URL, ShouldEqual, "https://www.google.com")
require.Nil(t, err)
require.Equal(t, "dingding_testing", notifier.Name)
require.Equal(t, "dingding", notifier.Type)
require.Equal(t, "https://www.google.com", notifier.URL)
Convey("genBody should not panic", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
State: models.AlertStateAlerting,
Message: `{host="localhost"}`,
}, &validations.OSSPluginRequestValidator{})
_, err = notifier.genBody(evalContext, "")
So(err, ShouldBeNil)
})
t.Run("genBody should not panic", func(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
State: models.AlertStateAlerting,
Message: `{host="localhost"}`,
}, &validations.OSSPluginRequestValidator{})
_, err = notifier.genBody(evalContext, "")
require.Nil(t, err)
})
})
}
+30 -31
View File
@@ -6,51 +6,50 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestDiscordNotifier(t *testing.T) {
Convey("Discord notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "discord_testing",
Type: "discord",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "discord_testing",
Type: "discord",
Settings: settingsJSON,
}
_, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("settings should trigger incident", func() {
json := `
t.Run("settings should trigger incident", func(t *testing.T) {
json := `
{
"avatar_url": "https://grafana.com/img/fav32.png",
"content": "@everyone Please check this notification",
"url": "https://web.hook/"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "discord_testing",
Type: "discord",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "discord_testing",
Type: "discord",
Settings: settingsJSON,
}
not, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
discordNotifier := not.(*DiscordNotifier)
not, err := newDiscordNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
discordNotifier := not.(*DiscordNotifier)
So(err, ShouldBeNil)
So(discordNotifier.Name, ShouldEqual, "discord_testing")
So(discordNotifier.Type, ShouldEqual, "discord")
So(discordNotifier.AvatarURL, ShouldEqual, "https://grafana.com/img/fav32.png")
So(discordNotifier.Content, ShouldEqual, "@everyone Please check this notification")
So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/")
})
require.Nil(t, err)
require.Equal(t, "discord_testing", discordNotifier.Name)
require.Equal(t, "discord", discordNotifier.Type)
require.Equal(t, "https://grafana.com/img/fav32.png", discordNotifier.AvatarURL)
require.Equal(t, "@everyone Please check this notification", discordNotifier.Content)
require.Equal(t, "https://web.hook/", discordNotifier.WebhookURL)
})
})
}
+46 -47
View File
@@ -6,74 +6,73 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestEmailNotifier(t *testing.T) {
Convey("Email notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
}
_, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("from settings", func() {
json := `
t.Run("from settings", func(t *testing.T) {
json := `
{
"addresses": "ops@grafana.org"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
}
not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
emailNotifier := not.(*EmailNotifier)
not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
emailNotifier := not.(*EmailNotifier)
So(err, ShouldBeNil)
So(emailNotifier.Name, ShouldEqual, "ops")
So(emailNotifier.Type, ShouldEqual, "email")
So(emailNotifier.Addresses[0], ShouldEqual, "ops@grafana.org")
})
require.Nil(t, err)
require.Equal(t, "ops", emailNotifier.Name)
require.Equal(t, "email", emailNotifier.Type)
require.Equal(t, "ops@grafana.org", emailNotifier.Addresses[0])
})
Convey("from settings with two emails", func() {
json := `
t.Run("from settings with two emails", func(t *testing.T) {
json := `
{
"addresses": "ops@grafana.org;dev@grafana.org"
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "ops",
Type: "email",
Settings: settingsJSON,
}
not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
emailNotifier := not.(*EmailNotifier)
not, err := NewEmailNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
emailNotifier := not.(*EmailNotifier)
So(err, ShouldBeNil)
So(emailNotifier.Name, ShouldEqual, "ops")
So(emailNotifier.Type, ShouldEqual, "email")
So(len(emailNotifier.Addresses), ShouldEqual, 2)
require.Nil(t, err)
require.Equal(t, "ops", emailNotifier.Name)
require.Equal(t, "email", emailNotifier.Type)
require.Equal(t, 2, len(emailNotifier.Addresses))
So(emailNotifier.Addresses[0], ShouldEqual, "ops@grafana.org")
So(emailNotifier.Addresses[1], ShouldEqual, "dev@grafana.org")
})
require.Equal(t, "ops@grafana.org", emailNotifier.Addresses[0])
require.Equal(t, "dev@grafana.org", emailNotifier.Addresses[1])
})
})
}
@@ -6,47 +6,46 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestGoogleChatNotifier(t *testing.T) {
Convey("Google Hangouts Chat notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "googlechat",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "googlechat",
Settings: settingsJSON,
}
_, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("from settings", func() {
json := `
t.Run("from settings", func(t *testing.T) {
json := `
{
"url": "http://google.com"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "googlechat",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "googlechat",
Settings: settingsJSON,
}
not, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
webhookNotifier := not.(*GoogleChatNotifier)
not, err := newGoogleChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
webhookNotifier := not.(*GoogleChatNotifier)
So(err, ShouldBeNil)
So(webhookNotifier.Name, ShouldEqual, "ops")
So(webhookNotifier.Type, ShouldEqual, "googlechat")
So(webhookNotifier.URL, ShouldEqual, "http://google.com")
})
require.Nil(t, err)
require.Equal(t, "ops", webhookNotifier.Name)
require.Equal(t, "googlechat", webhookNotifier.Type)
require.Equal(t, "http://google.com", webhookNotifier.URL)
})
})
}
+47 -48
View File
@@ -6,75 +6,74 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
//nolint:goconst
func TestHipChatNotifier(t *testing.T) {
Convey("HipChat notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
_, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("from settings", func() {
json := `
t.Run("from settings", func(t *testing.T) {
json := `
{
"url": "http://google.com"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
hipchatNotifier := not.(*HipChatNotifier)
not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
hipchatNotifier := not.(*HipChatNotifier)
So(err, ShouldBeNil)
So(hipchatNotifier.Name, ShouldEqual, "ops")
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
So(hipchatNotifier.URL, ShouldEqual, "http://google.com")
So(hipchatNotifier.APIKey, ShouldEqual, "")
So(hipchatNotifier.RoomID, ShouldEqual, "")
})
require.Nil(t, err)
require.Equal(t, "ops", hipchatNotifier.Name)
require.Equal(t, "hipchat", hipchatNotifier.Type)
require.Equal(t, "http://google.com", hipchatNotifier.URL)
require.Equal(t, "", hipchatNotifier.APIKey)
require.Equal(t, "", hipchatNotifier.RoomID)
})
Convey("from settings with Recipient and Mention", func() {
json := `
t.Run("from settings with Recipient and Mention", func(t *testing.T) {
json := `
{
"url": "http://www.hipchat.com",
"apikey": "1234",
"roomid": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "hipchat",
Settings: settingsJSON,
}
not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
hipchatNotifier := not.(*HipChatNotifier)
not, err := NewHipChatNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
hipchatNotifier := not.(*HipChatNotifier)
So(err, ShouldBeNil)
So(hipchatNotifier.Name, ShouldEqual, "ops")
So(hipchatNotifier.Type, ShouldEqual, "hipchat")
So(hipchatNotifier.URL, ShouldEqual, "http://www.hipchat.com")
So(hipchatNotifier.APIKey, ShouldEqual, "1234")
So(hipchatNotifier.RoomID, ShouldEqual, "1234")
})
require.Nil(t, err)
require.Equal(t, "ops", hipchatNotifier.Name)
require.Equal(t, "hipchat", hipchatNotifier.Type)
require.Equal(t, "http://www.hipchat.com", hipchatNotifier.URL)
require.Equal(t, "1234", hipchatNotifier.APIKey)
require.Equal(t, "1234", hipchatNotifier.RoomID)
})
})
}
+29 -30
View File
@@ -6,49 +6,48 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestKafkaNotifier(t *testing.T) {
Convey("Kafka notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "kafka_testing",
Type: "kafka",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "kafka_testing",
Type: "kafka",
Settings: settingsJSON,
}
_, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("settings should send an event to kafka", func() {
json := `
t.Run("settings should send an event to kafka", func(t *testing.T) {
json := `
{
"kafkaRestProxy": "http://localhost:8082",
"kafkaTopic": "topic1"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "kafka_testing",
Type: "kafka",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "kafka_testing",
Type: "kafka",
Settings: settingsJSON,
}
not, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
kafkaNotifier := not.(*KafkaNotifier)
not, err := NewKafkaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
kafkaNotifier := not.(*KafkaNotifier)
So(err, ShouldBeNil)
So(kafkaNotifier.Name, ShouldEqual, "kafka_testing")
So(kafkaNotifier.Type, ShouldEqual, "kafka")
So(kafkaNotifier.Endpoint, ShouldEqual, "http://localhost:8082")
So(kafkaNotifier.Topic, ShouldEqual, "topic1")
})
require.Nil(t, err)
require.Equal(t, "kafka_testing", kafkaNotifier.Name)
require.Equal(t, "kafka", kafkaNotifier.Type)
require.Equal(t, "http://localhost:8082", kafkaNotifier.Endpoint)
require.Equal(t, "topic1", kafkaNotifier.Topic)
})
})
}
+27 -28
View File
@@ -6,43 +6,42 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestLineNotifier(t *testing.T) {
Convey("Line notifier tests", t, func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "line_testing",
Type: "line",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "line_testing",
Type: "line",
Settings: settingsJSON,
}
_, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
Convey("settings should trigger incident", func() {
json := `
_, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
t.Run("settings should trigger incident", func(t *testing.T) {
json := `
{
"token": "abcdefgh0123456789"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "line_testing",
Type: "line",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "line_testing",
Type: "line",
Settings: settingsJSON,
}
not, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue)
lineNotifier := not.(*LineNotifier)
not, err := NewLINENotifier(model, ossencryption.ProvideService().GetDecryptedValue)
lineNotifier := not.(*LineNotifier)
So(err, ShouldBeNil)
So(lineNotifier.Name, ShouldEqual, "line_testing")
So(lineNotifier.Type, ShouldEqual, "line")
So(lineNotifier.Token, ShouldEqual, "abcdefgh0123456789")
})
require.Nil(t, err)
require.Equal(t, "line_testing", lineNotifier.Name)
require.Equal(t, "line", lineNotifier.Type)
require.Equal(t, "abcdefgh0123456789", lineNotifier.Token)
})
}
+173 -172
View File
@@ -2,6 +2,8 @@ package notifiers
import (
"context"
"reflect"
"strings"
"testing"
"github.com/grafana/grafana/pkg/bus"
@@ -10,217 +12,216 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/validations"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestOpsGenieNotifier(t *testing.T) {
Convey("OpsGenie notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
_, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("settings should trigger incident", func() {
json := `
t.Run("settings should trigger incident", func(t *testing.T) {
json := `
{
"apiKey": "abcdefgh0123456789"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
not, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
opsgenieNotifier := not.(*OpsGenieNotifier)
not, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
opsgenieNotifier := not.(*OpsGenieNotifier)
So(err, ShouldBeNil)
So(opsgenieNotifier.Name, ShouldEqual, "opsgenie_testing")
So(opsgenieNotifier.Type, ShouldEqual, "opsgenie")
So(opsgenieNotifier.APIKey, ShouldEqual, "abcdefgh0123456789")
})
require.Nil(t, err)
require.Equal(t, "opsgenie_testing", opsgenieNotifier.Name)
require.Equal(t, "opsgenie", opsgenieNotifier.Type)
require.Equal(t, "abcdefgh0123456789", opsgenieNotifier.APIKey)
})
})
Convey("Handling notification tags", func() {
Convey("invalid sendTagsAs value should return error", func() {
json := `{
t.Run("Handling notification tags", func(t *testing.T) {
t.Run("invalid sendTagsAs value should return error", func(t *testing.T) {
json := `{
"apiKey": "abcdefgh0123456789",
"sendTagsAs": "not_a_valid_value"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
_, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
So(err, ShouldHaveSameTypeAs, alerting.ValidationError{})
So(err.Error(), ShouldEndWith, "Invalid value for sendTagsAs: \"not_a_valid_value\"")
})
_, err := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
require.Equal(t, reflect.TypeOf(err), reflect.TypeOf(alerting.ValidationError{}))
require.True(t, strings.HasSuffix(err.Error(), "Invalid value for sendTagsAs: \"not_a_valid_value\""))
})
Convey("alert payload should include tag pairs only as an array in the tags key when sendAsTags is not set", func() {
json := `{
t.Run("alert payload should include tag pairs only as an array in the tags key when sendAsTags is not set", func(t *testing.T) {
json := `{
"apiKey": "abcdefgh0123456789"
}`
tagPairs := []*models.Tag{
{Key: "keyOnly"},
{Key: "aKey", Value: "aValue"},
tagPairs := []*models.Tag{
{Key: "keyOnly"},
{Key: "aKey", Value: "aValue"},
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandlerCtx("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandlerCtx("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
return err
})
alertErr := opsgenieNotifier.createAlert(evalContext)
So(notifierErr, ShouldBeNil)
So(alertErr, ShouldBeNil)
So(tags, ShouldResemble, []string{"keyOnly", "aKey:aValue"})
So(details, ShouldResemble, map[string]interface{}{"url": ""})
return err
})
Convey("alert payload should include tag pairs only as a map in the details key when sendAsTags=details", func() {
json := `{
alertErr := opsgenieNotifier.createAlert(evalContext)
require.Nil(t, notifierErr)
require.Nil(t, alertErr)
require.Equal(t, tags, []string{"keyOnly", "aKey:aValue"})
require.Equal(t, details, map[string]interface{}{"url": ""})
})
t.Run("alert payload should include tag pairs only as a map in the details key when sendAsTags=details", func(t *testing.T) {
json := `{
"apiKey": "abcdefgh0123456789",
"sendTagsAs": "details"
}`
tagPairs := []*models.Tag{
{Key: "keyOnly"},
{Key: "aKey", Value: "aValue"},
tagPairs := []*models.Tag{
{Key: "keyOnly"},
{Key: "aKey", Value: "aValue"},
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandlerCtx("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandlerCtx("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
return err
})
alertErr := opsgenieNotifier.createAlert(evalContext)
So(notifierErr, ShouldBeNil)
So(alertErr, ShouldBeNil)
So(tags, ShouldResemble, []string{})
So(details, ShouldResemble, map[string]interface{}{"keyOnly": "", "aKey": "aValue", "url": ""})
return err
})
Convey("alert payload should include tag pairs as both a map in the details key and an array in the tags key when sendAsTags=both", func() {
json := `{
alertErr := opsgenieNotifier.createAlert(evalContext)
require.Nil(t, notifierErr)
require.Nil(t, alertErr)
require.Equal(t, tags, []string{})
require.Equal(t, details, map[string]interface{}{"keyOnly": "", "aKey": "aValue", "url": ""})
})
t.Run("alert payload should include tag pairs as both a map in the details key and an array in the tags key when sendAsTags=both", func(t *testing.T) {
json := `{
"apiKey": "abcdefgh0123456789",
"sendTagsAs": "both"
}`
tagPairs := []*models.Tag{
{Key: "keyOnly"},
{Key: "aKey", Value: "aValue"},
tagPairs := []*models.Tag{
{Key: "keyOnly"},
{Key: "aKey", Value: "aValue"},
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandlerCtx("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "opsgenie_testing",
Type: "opsgenie",
Settings: settingsJSON,
}
notifier, notifierErr := NewOpsGenieNotifier(model, ossencryption.ProvideService().GetDecryptedValue) // unhandled error
opsgenieNotifier := notifier.(*OpsGenieNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: tagPairs,
}, nil)
evalContext.IsTestRun = true
tags := make([]string, 0)
details := make(map[string]interface{})
bus.AddHandlerCtx("alerting", func(ctx context.Context, cmd *models.SendWebhookSync) error {
bodyJSON, err := simplejson.NewJson([]byte(cmd.Body))
if err == nil {
tags = bodyJSON.Get("tags").MustStringArray([]string{})
details = bodyJSON.Get("details").MustMap(map[string]interface{}{})
}
return err
})
alertErr := opsgenieNotifier.createAlert(evalContext)
So(notifierErr, ShouldBeNil)
So(alertErr, ShouldBeNil)
So(tags, ShouldResemble, []string{"keyOnly", "aKey:aValue"})
So(details, ShouldResemble, map[string]interface{}{"keyOnly": "", "aKey": "aValue", "url": ""})
return err
})
alertErr := opsgenieNotifier.createAlert(evalContext)
require.Nil(t, notifierErr)
require.Nil(t, alertErr)
require.Equal(t, tags, []string{"keyOnly", "aKey:aValue"})
require.Equal(t, details, map[string]interface{}{"keyOnly": "", "aKey": "aValue", "url": ""})
})
})
}
+423 -426
View File
@@ -12,7 +12,8 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/validations"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func presenceComparer(a, b string) bool {
@@ -26,516 +27,512 @@ func presenceComparer(a, b string) bool {
}
func TestPagerdutyNotifier(t *testing.T) {
Convey("Pagerduty notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "pageduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pageduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
_, err = NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err = NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("severity should override default", func() {
json := `{ "integrationKey": "abcdefgh0123456789", "severity": "info", "tags": ["foo"]}`
t.Run("severity should override default", func(t *testing.T) {
json := `{ "integrationKey": "abcdefgh0123456789", "severity": "info", "tags": ["foo"]}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
pagerdutyNotifier := not.(*PagerdutyNotifier)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
pagerdutyNotifier := not.(*PagerdutyNotifier)
So(err, ShouldBeNil)
So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing")
So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty")
So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789")
So(pagerdutyNotifier.Severity, ShouldEqual, "info")
So(pagerdutyNotifier.AutoResolve, ShouldBeFalse)
})
require.Nil(t, err)
require.Equal(t, "pagerduty_testing", pagerdutyNotifier.Name)
require.Equal(t, "pagerduty", pagerdutyNotifier.Type)
require.Equal(t, "abcdefgh0123456789", pagerdutyNotifier.Key)
require.Equal(t, "info", pagerdutyNotifier.Severity)
require.False(t, pagerdutyNotifier.AutoResolve)
})
Convey("auto resolve and severity should have expected defaults", func() {
json := `{ "integrationKey": "abcdefgh0123456789" }`
t.Run("auto resolve and severity should have expected defaults", func(t *testing.T) {
json := `{ "integrationKey": "abcdefgh0123456789" }`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
pagerdutyNotifier := not.(*PagerdutyNotifier)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
pagerdutyNotifier := not.(*PagerdutyNotifier)
So(err, ShouldBeNil)
So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing")
So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty")
So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789")
So(pagerdutyNotifier.Severity, ShouldEqual, "critical")
So(pagerdutyNotifier.AutoResolve, ShouldBeFalse)
})
require.Nil(t, err)
require.Equal(t, "pagerduty_testing", pagerdutyNotifier.Name)
require.Equal(t, "pagerduty", pagerdutyNotifier.Type)
require.Equal(t, "abcdefgh0123456789", pagerdutyNotifier.Key)
require.Equal(t, "critical", pagerdutyNotifier.Severity)
require.False(t, pagerdutyNotifier.AutoResolve)
})
Convey("settings should trigger incident", func() {
json := `
t.Run("settings should trigger incident", func(t *testing.T) {
json := `
{
"integrationKey": "abcdefgh0123456789",
"autoResolve": false
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
pagerdutyNotifier := not.(*PagerdutyNotifier)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
pagerdutyNotifier := not.(*PagerdutyNotifier)
So(err, ShouldBeNil)
So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing")
So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty")
So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789")
So(pagerdutyNotifier.AutoResolve, ShouldBeFalse)
})
require.Nil(t, err)
require.Equal(t, "pagerduty_testing", pagerdutyNotifier.Name)
require.Equal(t, "pagerduty", pagerdutyNotifier.Type)
require.Equal(t, "abcdefgh0123456789", pagerdutyNotifier.Key)
require.False(t, pagerdutyNotifier.AutoResolve)
})
Convey("should return properly formatted default v2 event payload", func() {
json := `{
t.Run("should return properly formatted default v2 event payload", func(t *testing.T) {
json := `{
"integrationKey": "abcdefgh0123456789",
"autoResolve": false
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, err)
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
So(err, ShouldBeNil)
payload, err := simplejson.NewJson(payloadJSON)
So(err, ShouldBeNil)
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
require.Nil(t, err)
payload, err := simplejson.NewJson(payloadJSON)
require.Nil(t, err)
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"component": "Grafana",
"source": "<<PRESENCE>>",
"custom_details": map[string]interface{}{
"state": "alerting",
},
"severity": "critical",
"summary": "someRule - someMessage",
"timestamp": "<<PRESENCE>>",
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
So(diff, ShouldBeEmpty)
})
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"component": "Grafana",
"source": "<<PRESENCE>>",
"custom_details": map[string]interface{}{
"state": "alerting",
},
"severity": "critical",
"summary": "someRule - someMessage",
"timestamp": "<<PRESENCE>>",
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
require.Empty(t, diff)
})
Convey("should return properly formatted default v2 event payload with empty message", func() {
json := `{
t.Run("should return properly formatted default v2 event payload with empty message", func(t *testing.T) {
json := `{
"integrationKey": "abcdefgh0123456789",
"autoResolve": false
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, err)
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
So(err, ShouldBeNil)
payload, err := simplejson.NewJson(payloadJSON)
So(err, ShouldBeNil)
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
require.Nil(t, err)
payload, err := simplejson.NewJson(payloadJSON)
require.Nil(t, err)
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"component": "Grafana",
"source": "<<PRESENCE>>",
"custom_details": map[string]interface{}{
"state": "alerting",
},
"severity": "critical",
"summary": "someRule",
"timestamp": "<<PRESENCE>>",
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
So(diff, ShouldBeEmpty)
})
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"component": "Grafana",
"source": "<<PRESENCE>>",
"custom_details": map[string]interface{}{
"state": "alerting",
},
"severity": "critical",
"summary": "someRule",
"timestamp": "<<PRESENCE>>",
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
require.Empty(t, diff)
})
Convey("should return properly formatted payload with message moved to details", func() {
json := `{
t.Run("should return properly formatted payload with message moved to details", func(t *testing.T) {
json := `{
"integrationKey": "abcdefgh0123456789",
"autoResolve": false,
"messageInDetails": true
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, err)
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
evalContext.EvalMatches = []*alerting.EvalMatch{
{
// nil is a terrible value to test with, but the cmp.Diff doesn't
// like comparing actual floats. So this is roughly the equivalent
// of <<PRESENCE>>
Value: null.FloatFromPtr(nil),
Metric: "someMetric",
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
evalContext.EvalMatches = []*alerting.EvalMatch{
{
// nil is a terrible value to test with, but the cmp.Diff doesn't
// like comparing actual floats. So this is roughly the equivalent
// of <<PRESENCE>>
Value: null.FloatFromPtr(nil),
Metric: "someMetric",
},
}
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
require.NoError(t, err)
payload, err := simplejson.NewJson(payloadJSON)
require.NoError(t, err)
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"component": "Grafana",
"source": "<<PRESENCE>>",
"custom_details": map[string]interface{}{
"message": "someMessage",
"queries": map[string]interface{}{
"someMetric": nil,
},
}
"state": "alerting",
},
"severity": "critical",
"summary": "someRule",
"timestamp": "<<PRESENCE>>",
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
require.Empty(t, diff)
})
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
So(err, ShouldBeNil)
payload, err := simplejson.NewJson(payloadJSON)
So(err, ShouldBeNil)
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"component": "Grafana",
"source": "<<PRESENCE>>",
"custom_details": map[string]interface{}{
"message": "someMessage",
"queries": map[string]interface{}{
"someMetric": nil,
},
"state": "alerting",
},
"severity": "critical",
"summary": "someRule",
"timestamp": "<<PRESENCE>>",
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
So(diff, ShouldBeEmpty)
})
Convey("should return properly formatted v2 event payload when using override tags", func() {
json := `{
t.Run("should return properly formatted v2 event payload when using override tags", func(t *testing.T) {
json := `{
"integrationKey": "abcdefgh0123456789",
"autoResolve": false
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.NoError(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.NoError(t, err)
pagerdutyNotifier := not.(*PagerdutyNotifier)
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "group", Value: "aGroup"},
{Key: "class", Value: "aClass"},
{Key: "component", Value: "aComponent"},
{Key: "severity", Value: "warning"},
{Key: "dedup_key", Value: "key-" + strings.Repeat("x", 260)},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "group", Value: "aGroup"},
{Key: "class", Value: "aClass"},
{Key: "component", Value: "aComponent"},
{Key: "severity", Value: "warning"},
{Key: "dedup_key", Value: "key-" + strings.Repeat("x", 260)},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
So(err, ShouldBeNil)
payload, err := simplejson.NewJson(payloadJSON)
So(err, ShouldBeNil)
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
require.NoError(t, err)
payload, err := simplejson.NewJson(payloadJSON)
require.NoError(t, err)
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "key-" + strings.Repeat("x", 250),
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"source": "<<PRESENCE>>",
"component": "aComponent",
"custom_details": map[string]interface{}{
"group": "aGroup",
"class": "aClass",
"component": "aComponent",
"severity": "warning",
"dedup_key": "key-" + strings.Repeat("x", 250),
"keyOnly": "",
"state": "alerting",
},
"severity": "warning",
"summary": "someRule - someMessage",
"timestamp": "<<PRESENCE>>",
"class": "aClass",
"group": "aGroup",
},
"images": []interface{}{
map[string]interface{}{
"src": "http://somewhere.com/omg_dont_panic.png",
},
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
So(diff, ShouldBeEmpty)
})
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "key-" + strings.Repeat("x", 250),
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"source": "<<PRESENCE>>",
"component": "aComponent",
"custom_details": map[string]interface{}{
"group": "aGroup",
"class": "aClass",
"component": "aComponent",
"severity": "warning",
"dedup_key": "key-" + strings.Repeat("x", 250),
"keyOnly": "",
"state": "alerting",
},
"severity": "warning",
"summary": "someRule - someMessage",
"timestamp": "<<PRESENCE>>",
"class": "aClass",
"group": "aGroup",
},
"images": []interface{}{
map[string]interface{}{
"src": "http://somewhere.com/omg_dont_panic.png",
},
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
require.Empty(t, diff)
})
Convey("should support multiple levels of severity", func() {
json := `{
t.Run("should support multiple levels of severity", func(t *testing.T) {
json := `{
"integrationKey": "abcdefgh0123456789",
"autoResolve": false
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.NoError(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.NoError(t, err)
pagerdutyNotifier := not.(*PagerdutyNotifier)
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "group", Value: "aGroup"},
{Key: "class", Value: "aClass"},
{Key: "component", Value: "aComponent"},
{Key: "severity", Value: "info"},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "group", Value: "aGroup"},
{Key: "class", Value: "aClass"},
{Key: "component", Value: "aComponent"},
{Key: "severity", Value: "info"},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
So(err, ShouldBeNil)
payload, err := simplejson.NewJson(payloadJSON)
So(err, ShouldBeNil)
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
require.NoError(t, err)
payload, err := simplejson.NewJson(payloadJSON)
require.NoError(t, err)
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"source": "<<PRESENCE>>",
"component": "aComponent",
"custom_details": map[string]interface{}{
"group": "aGroup",
"class": "aClass",
"component": "aComponent",
"severity": "info",
"keyOnly": "",
"state": "alerting",
},
"severity": "info",
"summary": "someRule - someMessage",
"timestamp": "<<PRESENCE>>",
"class": "aClass",
"group": "aGroup",
},
"images": []interface{}{
map[string]interface{}{
"src": "http://somewhere.com/omg_dont_panic.png",
},
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
So(diff, ShouldBeEmpty)
})
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"source": "<<PRESENCE>>",
"component": "aComponent",
"custom_details": map[string]interface{}{
"group": "aGroup",
"class": "aClass",
"component": "aComponent",
"severity": "info",
"keyOnly": "",
"state": "alerting",
},
"severity": "info",
"summary": "someRule - someMessage",
"timestamp": "<<PRESENCE>>",
"class": "aClass",
"group": "aGroup",
},
"images": []interface{}{
map[string]interface{}{
"src": "http://somewhere.com/omg_dont_panic.png",
},
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
require.Empty(t, diff)
})
Convey("should ignore invalid severity for PD but keep the tag", func() {
json := `{
t.Run("should ignore invalid severity for PD but keep the tag", func(t *testing.T) {
json := `{
"integrationKey": "abcdefgh0123456789",
"autoResolve": false,
"severity": "critical"
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.NoError(t, err)
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "pagerduty_testing",
Type: "pagerduty",
Settings: settingsJSON,
}
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
not, err := NewPagerdutyNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.NoError(t, err)
pagerdutyNotifier := not.(*PagerdutyNotifier)
pagerdutyNotifier := not.(*PagerdutyNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "group", Value: "aGroup"},
{Key: "class", Value: "aClass"},
{Key: "component", Value: "aComponent"},
{Key: "severity", Value: "llama"},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "group", Value: "aGroup"},
{Key: "class", Value: "aClass"},
{Key: "component", Value: "aComponent"},
{Key: "severity", Value: "llama"},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.ImagePublicURL = "http://somewhere.com/omg_dont_panic.png"
evalContext.IsTestRun = true
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
So(err, ShouldBeNil)
payload, err := simplejson.NewJson(payloadJSON)
So(err, ShouldBeNil)
payloadJSON, err := pagerdutyNotifier.buildEventPayload(evalContext)
require.NoError(t, err)
payload, err := simplejson.NewJson(payloadJSON)
require.NoError(t, err)
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"source": "<<PRESENCE>>",
"component": "aComponent",
"custom_details": map[string]interface{}{
"group": "aGroup",
"class": "aClass",
"component": "aComponent",
"severity": "llama",
"keyOnly": "",
"state": "alerting",
},
"severity": "critical",
"summary": "someRule - someMessage",
"timestamp": "<<PRESENCE>>",
"class": "aClass",
"group": "aGroup",
},
"images": []interface{}{
map[string]interface{}{
"src": "http://somewhere.com/omg_dont_panic.png",
},
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
So(diff, ShouldBeEmpty)
})
})
diff := cmp.Diff(map[string]interface{}{
"client": "Grafana",
"client_url": "",
"dedup_key": "alertId-0",
"event_action": "trigger",
"links": []interface{}{
map[string]interface{}{
"href": "",
},
},
"payload": map[string]interface{}{
"source": "<<PRESENCE>>",
"component": "aComponent",
"custom_details": map[string]interface{}{
"group": "aGroup",
"class": "aClass",
"component": "aComponent",
"severity": "llama",
"keyOnly": "",
"state": "alerting",
},
"severity": "critical",
"summary": "someRule - someMessage",
"timestamp": "<<PRESENCE>>",
"class": "aClass",
"group": "aGroup",
},
"images": []interface{}{
map[string]interface{}{
"src": "http://somewhere.com/omg_dont_panic.png",
},
},
"routing_key": "abcdefgh0123456789",
}, payload.Interface(), cmp.Comparer(presenceComparer))
require.Empty(t, diff)
})
}
@@ -10,28 +10,28 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/validations"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestPushoverNotifier(t *testing.T) {
Convey("Pushover notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "Pushover",
Type: "pushover",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "Pushover",
Type: "pushover",
Settings: settingsJSON,
}
_, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("from settings", func() {
json := `
t.Run("from settings", func(t *testing.T) {
json := `
{
"apiToken": "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve",
"userKey": "tzNZYf36y0ohWwXo4XoUrB61rz1A4o",
@@ -41,58 +41,55 @@ func TestPushoverNotifier(t *testing.T) {
"okSound": "magic"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "Pushover",
Type: "pushover",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "Pushover",
Type: "pushover",
Settings: settingsJSON,
}
not, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
pushoverNotifier := not.(*PushoverNotifier)
not, err := NewPushoverNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
pushoverNotifier := not.(*PushoverNotifier)
So(err, ShouldBeNil)
So(pushoverNotifier.Name, ShouldEqual, "Pushover")
So(pushoverNotifier.Type, ShouldEqual, "pushover")
So(pushoverNotifier.APIToken, ShouldEqual, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve")
So(pushoverNotifier.UserKey, ShouldEqual, "tzNZYf36y0ohWwXo4XoUrB61rz1A4o")
So(pushoverNotifier.AlertingPriority, ShouldEqual, 1)
So(pushoverNotifier.OKPriority, ShouldEqual, 2)
So(pushoverNotifier.AlertingSound, ShouldEqual, "pushover")
So(pushoverNotifier.OKSound, ShouldEqual, "magic")
})
require.Nil(t, err)
require.Equal(t, "Pushover", pushoverNotifier.Name)
require.Equal(t, "pushover", pushoverNotifier.Type)
require.Equal(t, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve", pushoverNotifier.APIToken)
require.Equal(t, "tzNZYf36y0ohWwXo4XoUrB61rz1A4o", pushoverNotifier.UserKey)
require.Equal(t, 1, pushoverNotifier.AlertingPriority)
require.Equal(t, 2, pushoverNotifier.OKPriority)
require.Equal(t, "pushover", pushoverNotifier.AlertingSound)
require.Equal(t, "magic", pushoverNotifier.OKSound)
})
})
}
func TestGenPushoverBody(t *testing.T) {
Convey("Pushover body generation tests", t, func() {
Convey("Given common sounds", func() {
sirenSound := "siren_sound_tst"
successSound := "success_sound_tst"
notifier := &PushoverNotifier{AlertingSound: sirenSound, OKSound: successSound}
t.Run("Given common sounds", func(t *testing.T) {
sirenSound := "siren_sound_tst"
successSound := "success_sound_tst"
notifier := &PushoverNotifier{AlertingSound: sirenSound, OKSound: successSound}
Convey("When alert is firing - should use siren sound", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{})
_, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "")
t.Run("When alert is firing - should use siren sound", func(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
State: models.AlertStateAlerting,
}, &validations.OSSPluginRequestValidator{})
_, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "")
So(err, ShouldBeNil)
So(strings.Contains(pushoverBody.String(), sirenSound), ShouldBeTrue)
})
require.Nil(t, err)
require.True(t, strings.Contains(pushoverBody.String(), sirenSound))
})
Convey("When alert is ok - should use success sound", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
_, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "")
t.Run("When alert is ok - should use success sound", func(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
_, pushoverBody, err := notifier.genPushoverBody(evalContext, "", "")
So(err, ShouldBeNil)
So(strings.Contains(pushoverBody.String(), successSound), ShouldBeTrue)
})
require.Nil(t, err)
require.True(t, strings.Contains(pushoverBody.String(), successSound))
})
})
}
+30 -31
View File
@@ -6,51 +6,50 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestSensuNotifier(t *testing.T) {
Convey("Sensu notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "sensu",
Type: "sensu",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "sensu",
Type: "sensu",
Settings: settingsJSON,
}
_, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("from settings", func() {
json := `
t.Run("from settings", func(t *testing.T) {
json := `
{
"url": "http://sensu-api.example.com:4567/results",
"source": "grafana_instance_01",
"handler": "myhandler"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "sensu",
Type: "sensu",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "sensu",
Type: "sensu",
Settings: settingsJSON,
}
not, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
sensuNotifier := not.(*SensuNotifier)
not, err := NewSensuNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
sensuNotifier := not.(*SensuNotifier)
So(err, ShouldBeNil)
So(sensuNotifier.Name, ShouldEqual, "sensu")
So(sensuNotifier.Type, ShouldEqual, "sensu")
So(sensuNotifier.URL, ShouldEqual, "http://sensu-api.example.com:4567/results")
So(sensuNotifier.Source, ShouldEqual, "grafana_instance_01")
So(sensuNotifier.Handler, ShouldEqual, "myhandler")
})
require.Nil(t, err)
require.Equal(t, "sensu", sensuNotifier.Name)
require.Equal(t, "sensu", sensuNotifier.Type)
require.Equal(t, "http://sensu-api.example.com:4567/results", sensuNotifier.URL)
require.Equal(t, "grafana_instance_01", sensuNotifier.Source)
require.Equal(t, "myhandler", sensuNotifier.Handler)
})
})
}
+43 -44
View File
@@ -6,69 +6,68 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestTeamsNotifier(t *testing.T) {
Convey("Teams notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
}
_, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("from settings", func() {
json := `
t.Run("from settings", func(t *testing.T) {
json := `
{
"url": "http://google.com"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
}
not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
teamsNotifier := not.(*TeamsNotifier)
not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
teamsNotifier := not.(*TeamsNotifier)
So(err, ShouldBeNil)
So(teamsNotifier.Name, ShouldEqual, "ops")
So(teamsNotifier.Type, ShouldEqual, "teams")
So(teamsNotifier.URL, ShouldEqual, "http://google.com")
})
require.Nil(t, err)
require.Equal(t, "ops", teamsNotifier.Name)
require.Equal(t, "teams", teamsNotifier.Type)
require.Equal(t, "http://google.com", teamsNotifier.URL)
})
Convey("from settings with Recipient and Mention", func() {
json := `
t.Run("from settings with Recipient and Mention", func(t *testing.T) {
json := `
{
"url": "http://google.com"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "ops",
Type: "teams",
Settings: settingsJSON,
}
not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
teamsNotifier := not.(*TeamsNotifier)
not, err := NewTeamsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
teamsNotifier := not.(*TeamsNotifier)
So(err, ShouldBeNil)
So(teamsNotifier.Name, ShouldEqual, "ops")
So(teamsNotifier.Type, ShouldEqual, "teams")
So(teamsNotifier.URL, ShouldEqual, "http://google.com")
})
require.Nil(t, err)
require.Equal(t, "ops", teamsNotifier.Name)
require.Equal(t, "teams", teamsNotifier.Type)
require.Equal(t, "http://google.com", teamsNotifier.URL)
})
})
}
@@ -9,51 +9,67 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/validations"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestTelegramNotifier(t *testing.T) {
Convey("Telegram notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "telegram_testing",
Type: "telegram",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "telegram_testing",
Type: "telegram",
Settings: settingsJSON,
}
_, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("settings should trigger incident", func() {
json := `
t.Run("settings should trigger incident", func(t *testing.T) {
json := `
{
"bottoken": "abcdefgh0123456789",
"chatid": "-1234567890"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "telegram_testing",
Type: "telegram",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "telegram_testing",
Type: "telegram",
Settings: settingsJSON,
}
not, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
telegramNotifier := not.(*TelegramNotifier)
not, err := NewTelegramNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
telegramNotifier := not.(*TelegramNotifier)
So(err, ShouldBeNil)
So(telegramNotifier.Name, ShouldEqual, "telegram_testing")
So(telegramNotifier.Type, ShouldEqual, "telegram")
So(telegramNotifier.BotToken, ShouldEqual, "abcdefgh0123456789")
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
})
require.Nil(t, err)
require.Equal(t, "telegram_testing", telegramNotifier.Name)
require.Equal(t, "telegram", telegramNotifier.Type)
require.Equal(t, "abcdefgh0123456789", telegramNotifier.BotToken)
require.Equal(t, "-1234567890", telegramNotifier.ChatID)
})
Convey("generateCaption should generate a message with all pertinent details", func() {
t.Run("generateCaption should generate a message with all pertinent details", func(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message.",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
require.LessOrEqual(t, len(caption), 1024)
require.Contains(t, caption, "Some kind of message.")
require.Contains(t, caption, "[OK] This is an alarm")
require.Contains(t, caption, "http://grafa.url/abcdef")
})
t.Run("When generating a message", func(t *testing.T) {
t.Run("URL should be skipped if it's too long", func(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
@@ -61,65 +77,48 @@ func TestTelegramNotifier(t *testing.T) {
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
So(caption, ShouldContainSubstring, "Some kind of message.")
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldContainSubstring, "http://grafa.url/abcdef")
caption := generateImageCaption(evalContext,
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"foo bar")
require.LessOrEqual(t, len(caption), 1024)
require.Contains(t, caption, "Some kind of message.")
require.Contains(t, caption, "[OK] This is an alarm")
require.Contains(t, caption, "foo bar")
require.NotContains(t, caption, "http")
})
Convey("When generating a message", func() {
Convey("URL should be skipped if it's too long", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message.",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
t.Run("Message should be trimmed if it's too long", func(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis scelerisque. Nulla ipsum ex, iaculis vitae vehicula sit amet, fermentum eu eros.",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
caption := generateImageCaption(evalContext,
"http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"foo bar")
So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
So(caption, ShouldContainSubstring, "Some kind of message.")
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldContainSubstring, "foo bar")
So(caption, ShouldNotContainSubstring, "http")
})
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
"")
require.LessOrEqual(t, len(caption), 1024)
require.Contains(t, caption, "[OK] This is an alarm")
require.NotContains(t, caption, "http")
require.Contains(t, caption, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri")
})
Convey("Message should be trimmed if it's too long", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis scelerisque. Nulla ipsum ex, iaculis vitae vehicula sit amet, fermentum eu eros.",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
t.Run("Metrics should be skipped if they don't fit", func(t *testing.T) {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
"")
So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldNotContainSubstring, "http")
So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri")
})
Convey("Metrics should be skipped if they don't fit", func() {
evalContext := alerting.NewEvalContext(context.Background(),
&alerting.Rule{
Name: "This is an alarm",
Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it. But suddenly Telegram increased the length so now we need some lorem ipsum to fix this test. Here we go: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus consectetur molestie cursus. Donec suscipit egestas nisi. Proin ut efficitur ex. Mauris mi augue, volutpat a nisi vel, euismod dictum arcu. Sed quis tempor eros, sed malesuada dolor. Ut orci augue, viverra sit amet blandit quis, faucibus sit amet ex. Duis condimentum efficitur lectus, id dignissim quam tempor id. Morbi sollicitudin rhoncus diam, id tincidunt lectus scelerisque vitae. Etiam imperdiet semper sem, vel eleifend ligula mollis eget. Etiam ultrices fringilla lacus, sit amet pharetra ex blandit quis. Suspendisse in egestas neque, et posuere lectus. Vestibulum eu ex dui. Sed molestie nulla a lobortis sceleri",
State: models.AlertStateOK,
}, &validations.OSSPluginRequestValidator{})
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
"foo bar long song")
So(len(caption), ShouldBeLessThanOrEqualTo, 1024)
So(caption, ShouldContainSubstring, "[OK] This is an alarm")
So(caption, ShouldNotContainSubstring, "http")
So(caption, ShouldNotContainSubstring, "foo bar")
})
caption := generateImageCaption(evalContext,
"http://grafa.url/foo",
"foo bar long song")
require.LessOrEqual(t, len(caption), 1024)
require.Contains(t, caption, "[OK] This is an alarm")
require.NotContains(t, caption, "http")
require.NotContains(t, caption, "foo bar")
})
})
})
+73 -74
View File
@@ -8,118 +8,117 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestThreemaNotifier(t *testing.T) {
Convey("Threema notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
_, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("valid settings should be parsed successfully", func() {
json := `
t.Run("valid settings should be parsed successfully", func(t *testing.T) {
json := `
{
"gateway_id": "*3MAGWID",
"recipient_id": "ECHOECHO",
"api_secret": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
threemaNotifier := not.(*ThreemaNotifier)
not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, err)
threemaNotifier := not.(*ThreemaNotifier)
So(err, ShouldBeNil)
So(threemaNotifier.Name, ShouldEqual, "threema_testing")
So(threemaNotifier.Type, ShouldEqual, "threema")
So(threemaNotifier.GatewayID, ShouldEqual, "*3MAGWID")
So(threemaNotifier.RecipientID, ShouldEqual, "ECHOECHO")
So(threemaNotifier.APISecret, ShouldEqual, "1234")
})
require.Nil(t, err)
require.Equal(t, "threema_testing", threemaNotifier.Name)
require.Equal(t, "threema", threemaNotifier.Type)
require.Equal(t, "*3MAGWID", threemaNotifier.GatewayID)
require.Equal(t, "ECHOECHO", threemaNotifier.RecipientID)
require.Equal(t, "1234", threemaNotifier.APISecret)
})
Convey("invalid Threema Gateway IDs should be rejected (prefix)", func() {
json := `
t.Run("invalid Threema Gateway IDs should be rejected (prefix)", func(t *testing.T) {
json := `
{
"gateway_id": "ECHOECHO",
"recipient_id": "ECHOECHO",
"api_secret": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(not, ShouldBeNil)
var valErr alerting.ValidationError
So(errors.As(err, &valErr), ShouldBeTrue)
So(valErr.Reason, ShouldEqual, "Invalid Threema Gateway ID: Must start with a *")
})
not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, not)
var valErr alerting.ValidationError
require.True(t, errors.As(err, &valErr))
require.Equal(t, "Invalid Threema Gateway ID: Must start with a *", valErr.Reason)
})
Convey("invalid Threema Gateway IDs should be rejected (length)", func() {
json := `
t.Run("invalid Threema Gateway IDs should be rejected (length)", func(t *testing.T) {
json := `
{
"gateway_id": "*ECHOECHO",
"recipient_id": "ECHOECHO",
"api_secret": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(not, ShouldBeNil)
var valErr alerting.ValidationError
So(errors.As(err, &valErr), ShouldBeTrue)
So(valErr.Reason, ShouldEqual, "Invalid Threema Gateway ID: Must be 8 characters long")
})
not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, not)
var valErr alerting.ValidationError
require.True(t, errors.As(err, &valErr))
require.Equal(t, "Invalid Threema Gateway ID: Must be 8 characters long", valErr.Reason)
})
Convey("invalid Threema Recipient IDs should be rejected (length)", func() {
json := `
t.Run("invalid Threema Recipient IDs should be rejected (length)", func(t *testing.T) {
json := `
{
"gateway_id": "*3MAGWID",
"recipient_id": "ECHOECH",
"api_secret": "1234"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "threema_testing",
Type: "threema",
Settings: settingsJSON,
}
not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(not, ShouldBeNil)
var valErr alerting.ValidationError
So(errors.As(err, &valErr), ShouldBeTrue)
So(valErr.Reason, ShouldEqual, "Invalid Threema Recipient ID: Must be 8 characters long")
})
not, err := NewThreemaNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, not)
var valErr alerting.ValidationError
require.True(t, errors.As(err, &valErr))
require.Equal(t, "Invalid Threema Recipient ID: Must be 8 characters long", valErr.Reason)
})
})
}
+104 -105
View File
@@ -10,7 +10,8 @@ import (
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/encryption/ossencryption"
"github.com/grafana/grafana/pkg/services/validations"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func presenceComparerInt(a, b int64) bool {
@@ -23,140 +24,138 @@ func presenceComparerInt(a, b int64) bool {
return a == b
}
func TestVictoropsNotifier(t *testing.T) {
Convey("Victorops notifier tests", t, func() {
Convey("Parsing alert notification from settings", func() {
Convey("empty settings should return error", func() {
json := `{ }`
t.Run("Parsing alert notification from settings", func(t *testing.T) {
t.Run("empty settings should return error", func(t *testing.T) {
json := `{ }`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
}
_, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldNotBeNil)
})
_, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Error(t, err)
})
Convey("from settings", func() {
json := `
t.Run("from settings", func(t *testing.T) {
json := `
{
"url": "http://google.com"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
}
settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
}
not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
victoropsNotifier := not.(*VictoropsNotifier)
not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
victoropsNotifier := not.(*VictoropsNotifier)
So(err, ShouldBeNil)
So(victoropsNotifier.Name, ShouldEqual, "victorops_testing")
So(victoropsNotifier.Type, ShouldEqual, "victorops")
So(victoropsNotifier.URL, ShouldEqual, "http://google.com")
})
require.Nil(t, err)
require.Equal(t, "victorops_testing", victoropsNotifier.Name)
require.Equal(t, "victorops", victoropsNotifier.Type)
require.Equal(t, "http://google.com", victoropsNotifier.URL)
})
Convey("should return properly formatted event payload when using severity override tag", func() {
json := `
t.Run("should return properly formatted event payload when using severity override tag", func(t *testing.T) {
json := `
{
"url": "http://google.com"
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
}
not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, err)
victoropsNotifier := not.(*VictoropsNotifier)
victoropsNotifier := not.(*VictoropsNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "severity", Value: "warning"},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateAlerting,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "severity", Value: "warning"},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
payload, err := victoropsNotifier.buildEventPayload(evalContext)
So(err, ShouldBeNil)
payload, err := victoropsNotifier.buildEventPayload(evalContext)
require.Nil(t, err)
diff := cmp.Diff(map[string]interface{}{
"alert_url": "",
"entity_display_name": "[Alerting] someRule",
"entity_id": "someRule",
"message_type": "WARNING",
"metrics": map[string]interface{}{},
"monitoring_tool": "Grafana v",
"state_message": "someMessage",
"state_start_time": int64(-1),
"timestamp": int64(-1),
}, payload.Interface(), cmp.Comparer(presenceComparerInt))
So(diff, ShouldBeEmpty)
})
Convey("resolving with severity works properly", func() {
json := `
diff := cmp.Diff(map[string]interface{}{
"alert_url": "",
"entity_display_name": "[Alerting] someRule",
"entity_id": "someRule",
"message_type": "WARNING",
"metrics": map[string]interface{}{},
"monitoring_tool": "Grafana v",
"state_message": "someMessage",
"state_start_time": int64(-1),
"timestamp": int64(-1),
}, payload.Interface(), cmp.Comparer(presenceComparerInt))
require.Empty(t, diff)
})
t.Run("resolving with severity works properly", func(t *testing.T) {
json := `
{
"url": "http://google.com"
}`
settingsJSON, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
settingsJSON, err := simplejson.NewJson([]byte(json))
require.Nil(t, err)
model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
}
model := &models.AlertNotification{
Name: "victorops_testing",
Type: "victorops",
Settings: settingsJSON,
}
not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
So(err, ShouldBeNil)
not, err := NewVictoropsNotifier(model, ossencryption.ProvideService().GetDecryptedValue)
require.Nil(t, err)
victoropsNotifier := not.(*VictoropsNotifier)
victoropsNotifier := not.(*VictoropsNotifier)
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateOK,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "severity", Value: "warning"},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
evalContext := alerting.NewEvalContext(context.Background(), &alerting.Rule{
ID: 0,
Name: "someRule",
Message: "someMessage",
State: models.AlertStateOK,
AlertRuleTags: []*models.Tag{
{Key: "keyOnly"},
{Key: "severity", Value: "warning"},
},
}, &validations.OSSPluginRequestValidator{})
evalContext.IsTestRun = true
payload, err := victoropsNotifier.buildEventPayload(evalContext)
So(err, ShouldBeNil)
payload, err := victoropsNotifier.buildEventPayload(evalContext)
require.Nil(t, err)
diff := cmp.Diff(map[string]interface{}{
"alert_url": "",
"entity_display_name": "[OK] someRule",
"entity_id": "someRule",
"message_type": "RECOVERY",
"metrics": map[string]interface{}{},
"monitoring_tool": "Grafana v",
"state_message": "someMessage",
"state_start_time": int64(-1),
"timestamp": int64(-1),
}, payload.Interface(), cmp.Comparer(presenceComparerInt))
So(diff, ShouldBeEmpty)
})
diff := cmp.Diff(map[string]interface{}{
"alert_url": "",
"entity_display_name": "[OK] someRule",
"entity_id": "someRule",
"message_type": "RECOVERY",
"metrics": map[string]interface{}{},
"monitoring_tool": "Grafana v",
"state_message": "someMessage",
"state_start_time": int64(-1),
"timestamp": int64(-1),
}, payload.Interface(), cmp.Comparer(presenceComparerInt))
require.Empty(t, diff)
})
})
}
@@ -97,6 +97,7 @@ func TestPatchLibraryElement(t *testing.T) {
sc.initialResult.Result.FolderID = newFolder.Id
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@@ -117,6 +118,7 @@ func TestPatchLibraryElement(t *testing.T) {
sc.initialResult.Result.Name = "New Name"
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated
sc.initialResult.Result.Model["title"] = "Text - Library Panel"
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
@@ -138,6 +140,7 @@ func TestPatchLibraryElement(t *testing.T) {
sc.initialResult.Result.UID = cmd.UID
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated
sc.initialResult.Result.Model["title"] = "Text - Library Panel"
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
@@ -209,6 +212,7 @@ func TestPatchLibraryElement(t *testing.T) {
}
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@@ -234,6 +238,7 @@ func TestPatchLibraryElement(t *testing.T) {
}
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@@ -259,28 +264,29 @@ func TestPatchLibraryElement(t *testing.T) {
}
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
// Test is disabled currently due to flaky comparison
// scenarioWithPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result",
// func(t *testing.T, sc scenarioContext) {
// cmd := patchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(models.PanelElement)}
// sc.reqContext.UserId = 2
// sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
// resp := sc.service.patchHandler(sc.reqContext, cmd)
// var result = validateAndUnMarshalResponse(t, resp)
// sc.initialResult.Result.Meta.UpdatedBy.ID = int64(2)
// sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
// sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
// sc.initialResult.Result.Version = 2
// if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
// t.Fatalf("Result mismatch (-want +got):\n%s", diff)
// }
// })
scenarioWithPanel(t, "When another admin tries to patch a library panel, it should change UpdatedBy successfully and return correct result",
func(t *testing.T, sc scenarioContext) {
cmd := patchLibraryElementCommand{FolderID: -1, Version: 1, Kind: int64(models.PanelElement)}
sc.reqContext.UserId = 2
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.patchHandler(sc.reqContext, cmd)
var result = validateAndUnMarshalResponse(t, resp)
sc.initialResult.Result.Meta.UpdatedBy.ID = int64(2)
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
})
scenarioWithPanel(t, "When an admin tries to patch a library panel with a name that already exists, it should fail",
func(t *testing.T, sc scenarioContext) {
@@ -363,6 +369,7 @@ func TestPatchLibraryElement(t *testing.T) {
}
sc.initialResult.Result.Meta.CreatedBy.Name = userInDbName
sc.initialResult.Result.Meta.CreatedBy.AvatarURL = userInDbAvatar
sc.initialResult.Result.Meta.Updated = result.Result.Meta.Updated
sc.initialResult.Result.Version = 2
if diff := cmp.Diff(sc.initialResult.Result, result.Result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
+6 -2
View File
@@ -443,8 +443,12 @@ func contextWithTimeoutFromRequest(ctx context.Context, r *http.Request, default
func newTestReceiversResult(r *notifier.TestReceiversResult) apimodels.TestReceiversResult {
v := apimodels.TestReceiversResult{
Receivers: make([]apimodels.TestReceiverResult, len(r.Receivers)),
NotifedAt: r.NotifedAt,
Alert: apimodels.TestReceiversConfigAlertParams{
Annotations: r.Alert.Annotations,
Labels: r.Alert.Labels,
},
Receivers: make([]apimodels.TestReceiverResult, len(r.Receivers)),
NotifiedAt: r.NotifedAt,
}
for ix, next := range r.Receivers {
configs := make([]apimodels.TestReceiverConfigResult, len(next.Configs))
@@ -137,6 +137,8 @@ type MultiStatus struct{}
// swagger:parameters RoutePostTestReceivers
type TestReceiversConfigParams struct {
// in:body
Alert *TestReceiversConfigAlertParams `yaml:"alert,omitempty" json:"alert,omitempty"`
// in:body
Receivers []*PostableApiReceiver `yaml:"receivers,omitempty" json:"receivers,omitempty"`
}
@@ -145,10 +147,16 @@ func (c *TestReceiversConfigParams) ProcessConfig(encrypt EncryptFn) error {
return processReceiverConfigs(c.Receivers, encrypt)
}
type TestReceiversConfigAlertParams struct {
Annotations model.LabelSet `yaml:"annotations,omitempty" json:"annotations,omitempty"`
Labels model.LabelSet `yaml:"labels,omitempty" json:"labels,omitempty"`
}
// swagger:model
type TestReceiversResult struct {
Receivers []TestReceiverResult `json:"receivers"`
NotifedAt time.Time `json:"notified_at"`
Alert TestReceiversConfigAlertParams `json:"alert"`
Receivers []TestReceiverResult `json:"receivers"`
NotifiedAt time.Time `json:"notified_at"`
}
// swagger:model
+48 -10
View File
@@ -1065,6 +1065,19 @@
"type": "array",
"x-go-package": "github.com/prometheus/common/model"
},
"LabelSet": {
"additionalProperties": {
"$ref": "#/definitions/LabelValue"
},
"description": "A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet\nmay be fully-qualified down to the point where it may resolve to a single\nMetric in the data store or not. All operations that occur within the realm\nof a LabelSet can emit a vector of Metric entities to which the LabelSet may\nmatch.",
"type": "object",
"x-go-package": "github.com/prometheus/common/model"
},
"LabelValue": {
"title": "A LabelValue is an associated value for a LabelName.",
"type": "string",
"x-go-package": "github.com/prometheus/common/model"
},
"Labels": {
"description": "Labels is a sorted set of labels. Order has to be guaranteed upon\ninstantiation.",
"items": {
@@ -2403,12 +2416,27 @@
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"TestReceiversConfigAlertParams": {
"properties": {
"annotations": {
"$ref": "#/definitions/LabelSet"
},
"labels": {
"$ref": "#/definitions/LabelSet"
}
},
"type": "object",
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"TestReceiversResult": {
"properties": {
"alert": {
"$ref": "#/definitions/TestReceiversConfigAlertParams"
},
"notified_at": {
"format": "date-time",
"type": "string",
"x-go-name": "NotifedAt"
"x-go-name": "NotifiedAt"
},
"receivers": {
"items": {
@@ -2649,6 +2677,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
"description": "AlertGroup alert group",
"properties": {
"alerts": {
"description": "alerts",
@@ -2670,9 +2699,7 @@
"labels",
"receiver"
],
"type": "object",
"x-go-name": "AlertGroup",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
"type": "object"
},
"alertGroups": {
"items": {
@@ -2869,7 +2896,6 @@
"type": "array"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"properties": {
"comment": {
"description": "comment",
@@ -2921,14 +2947,17 @@
"status",
"updatedAt"
],
"type": "object"
"type": "object",
"x-go-name": "GettableSilence",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"items": {
"$ref": "#/definitions/gettableSilence"
},
"type": "array"
"type": "array",
"x-go-name": "GettableSilences",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"labelSet": {
"additionalProperties": {
@@ -3101,7 +3130,6 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"receiver": {
"description": "Receiver receiver",
"properties": {
"name": {
"description": "name",
@@ -3112,7 +3140,9 @@
"required": [
"name"
],
"type": "object"
"type": "object",
"x-go-name": "Receiver",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"silence": {
"description": "Silence silence",
@@ -3699,6 +3729,14 @@
"post": {
"operationId": "RoutePostTestReceivers",
"parameters": [
{
"in": "body",
"name": "alert",
"schema": {
"$ref": "#/definitions/TestReceiversConfigAlertParams"
},
"x-go-name": "Alert"
},
{
"in": "body",
"name": "receivers",
+44 -6
View File
@@ -493,6 +493,14 @@
"summary": "Test Grafana managed receivers without saving them.",
"operationId": "RoutePostTestReceivers",
"parameters": [
{
"x-go-name": "Alert",
"name": "alert",
"in": "body",
"schema": {
"$ref": "#/definitions/TestReceiversConfigAlertParams"
}
},
{
"x-go-name": "Receivers",
"name": "receivers",
@@ -2090,6 +2098,19 @@
},
"x-go-package": "github.com/prometheus/common/model"
},
"LabelSet": {
"description": "A LabelSet is a collection of LabelName and LabelValue pairs. The LabelSet\nmay be fully-qualified down to the point where it may resolve to a single\nMetric in the data store or not. All operations that occur within the realm\nof a LabelSet can emit a vector of Metric entities to which the LabelSet may\nmatch.",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/LabelValue"
},
"x-go-package": "github.com/prometheus/common/model"
},
"LabelValue": {
"type": "string",
"title": "A LabelValue is an associated value for a LabelName.",
"x-go-package": "github.com/prometheus/common/model"
},
"Labels": {
"description": "Labels is a sorted set of labels. Order has to be guaranteed upon\ninstantiation.",
"type": "array",
@@ -3429,13 +3450,28 @@
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"TestReceiversConfigAlertParams": {
"type": "object",
"properties": {
"annotations": {
"$ref": "#/definitions/LabelSet"
},
"labels": {
"$ref": "#/definitions/LabelSet"
}
},
"x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
},
"TestReceiversResult": {
"type": "object",
"properties": {
"alert": {
"$ref": "#/definitions/TestReceiversConfigAlertParams"
},
"notified_at": {
"type": "string",
"format": "date-time",
"x-go-name": "NotifedAt"
"x-go-name": "NotifiedAt"
},
"receivers": {
"type": "array",
@@ -3675,6 +3711,7 @@
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models"
},
"alertGroup": {
"description": "AlertGroup alert group",
"type": "object",
"required": [
"alerts",
@@ -3697,8 +3734,6 @@
"$ref": "#/definitions/receiver"
}
},
"x-go-name": "AlertGroup",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/alertGroup"
},
"alertGroups": {
@@ -3899,7 +3934,6 @@
"$ref": "#/definitions/gettableAlerts"
},
"gettableSilence": {
"description": "GettableSilence gettable silence",
"type": "object",
"required": [
"comment",
@@ -3952,14 +3986,17 @@
"x-go-name": "UpdatedAt"
}
},
"x-go-name": "GettableSilence",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableSilence"
},
"gettableSilences": {
"description": "GettableSilences gettable silences",
"type": "array",
"items": {
"$ref": "#/definitions/gettableSilence"
},
"x-go-name": "GettableSilences",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/gettableSilences"
},
"labelSet": {
@@ -4134,7 +4171,6 @@
"$ref": "#/definitions/postableSilence"
},
"receiver": {
"description": "Receiver receiver",
"type": "object",
"required": [
"name"
@@ -4146,6 +4182,8 @@
"x-go-name": "Name"
}
},
"x-go-name": "Receiver",
"x-go-package": "github.com/prometheus/alertmanager/api/v2/models",
"$ref": "#/definitions/receiver"
},
"silence": {
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
@@ -48,7 +47,7 @@ func NewAlertmanagerNotifier(model *NotificationChannelConfig, _ *template.Templ
basicAuthPassword := fn(context.Background(), model.SecureSettings, "basicAuthPassword", model.Settings.Get("basicAuthPassword").MustString())
return &AlertmanagerNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
DisableResolveMessage: model.DisableResolveMessage,
@@ -63,7 +62,7 @@ func NewAlertmanagerNotifier(model *NotificationChannelConfig, _ *template.Templ
// AlertmanagerNotifier sends alert notifications to the alert manager
type AlertmanagerNotifier struct {
old_notifiers.NotifierBase
*Base
urls []*url.URL
basicAuthUser string
@@ -0,0 +1,32 @@
package channels
import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
)
// Base is the base implementation of a notifier. It contains the common fields across all notifier types.
type Base struct {
Name string
Type string
UID string
IsDefault bool
DisableResolveMessage bool
log log.Logger
}
func (n *Base) GetDisableResolveMessage() bool {
return n.DisableResolveMessage
}
func NewBase(model *models.AlertNotification) *Base {
return &Base{
UID: model.Uid,
Name: model.Name,
IsDefault: model.IsDefault,
Type: model.Type,
DisableResolveMessage: model.DisableResolveMessage,
log: log.New("alerting.notifier." + model.Name),
}
}
@@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
)
const defaultDingdingMsgType = "link"
@@ -31,7 +30,7 @@ func NewDingDingNotifier(model *NotificationChannelConfig, t *template.Template)
msgType := model.Settings.Get("msgType").MustString(defaultDingdingMsgType)
return &DingDingNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -48,7 +47,7 @@ func NewDingDingNotifier(model *NotificationChannelConfig, t *template.Template)
// DingDingNotifier is responsible for sending alert notifications to ding ding.
type DingDingNotifier struct {
old_notifiers.NotifierBase
*Base
MsgType string
URL string
Message string
@@ -13,12 +13,11 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/grafana/grafana/pkg/setting"
)
type DiscordNotifier struct {
old_notifiers.NotifierBase
*Base
log log.Logger
tmpl *template.Template
Content string
@@ -41,7 +40,7 @@ func NewDiscordNotifier(model *NotificationChannelConfig, t *template.Template)
content := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`)
return &DiscordNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -11,14 +11,13 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/grafana/grafana/pkg/util"
)
// EmailNotifier is responsible for sending
// alert notifications over email.
type EmailNotifier struct {
old_notifiers.NotifierBase
*Base
Addresses []string
SingleEmail bool
Message string
@@ -44,7 +43,7 @@ func NewEmailNotifier(model *NotificationChannelConfig, t *template.Template) (*
addresses := util.SplitEmails(addressesString)
return &EmailNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -12,14 +12,13 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/grafana/grafana/pkg/setting"
)
// GoogleChatNotifier is responsible for sending
// alert notifications to Google chat.
type GoogleChatNotifier struct {
old_notifiers.NotifierBase
*Base
URL string
log log.Logger
tmpl *template.Template
@@ -32,7 +31,7 @@ func NewGoogleChatNotifier(model *NotificationChannelConfig, t *template.Templat
}
return &GoogleChatNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -13,13 +13,12 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
)
// KafkaNotifier is responsible for sending
// alert notifications to Kafka.
type KafkaNotifier struct {
old_notifiers.NotifierBase
*Base
Endpoint string
Topic string
log log.Logger
@@ -38,7 +37,7 @@ func NewKafkaNotifier(model *NotificationChannelConfig, t *template.Template) (*
}
return &KafkaNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
@@ -26,7 +25,7 @@ func NewLineNotifier(model *NotificationChannelConfig, t *template.Template, fn
}
return &LineNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -42,7 +41,7 @@ func NewLineNotifier(model *NotificationChannelConfig, t *template.Template, fn
// LineNotifier is responsible for sending
// alert notifications to LINE.
type LineNotifier struct {
old_notifiers.NotifierBase
*Base
Token string
log log.Logger
tmpl *template.Template
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
@@ -31,7 +30,7 @@ var (
// OpsgenieNotifier is responsible for sending alert notifications to Opsgenie.
type OpsgenieNotifier struct {
old_notifiers.NotifierBase
*Base
APIKey string
APIUrl string
AutoClose bool
@@ -62,7 +61,7 @@ func NewOpsgenieNotifier(model *NotificationChannelConfig, t *template.Template,
}
return &OpsgenieNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
@@ -28,7 +27,7 @@ var (
// PagerdutyNotifier is responsible for sending
// alert notifications to pagerduty
type PagerdutyNotifier struct {
old_notifiers.NotifierBase
*Base
Key string
Severity string
CustomDetails map[string]string
@@ -52,7 +51,7 @@ func NewPagerdutyNotifier(model *NotificationChannelConfig, t *template.Template
}
return &PagerdutyNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -10,7 +10,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
@@ -23,7 +22,7 @@ var (
// PushoverNotifier is responsible for sending
// alert notifications to Pushover
type PushoverNotifier struct {
old_notifiers.NotifierBase
*Base
UserKey string
APIToken string
AlertingPriority int
@@ -69,7 +68,7 @@ func NewPushoverNotifier(model *NotificationChannelConfig, t *template.Template,
return nil, receiverInitError{Cfg: *model, Reason: "API token not found"}
}
return &PushoverNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -10,14 +10,13 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
)
type SensuGoNotifier struct {
old_notifiers.NotifierBase
*Base
log log.Logger
tmpl *template.Template
@@ -47,7 +46,7 @@ func NewSensuGoNotifier(model *NotificationChannelConfig, t *template.Template,
}
return &SensuGoNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/template"
@@ -26,7 +25,7 @@ import (
// SlackNotifier is responsible for sending
// alert notification to Slack.
type SlackNotifier struct {
old_notifiers.NotifierBase
*Base
log log.Logger
tmpl *template.Template
@@ -106,7 +105,7 @@ func NewSlackNotifier(model *NotificationChannelConfig, t *template.Template, fn
}
return &SlackNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -11,13 +11,12 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
)
// TeamsNotifier is responsible for sending
// alert notifications to Microsoft teams.
type TeamsNotifier struct {
old_notifiers.NotifierBase
*Base
URL string
Message string
tmpl *template.Template
@@ -36,7 +35,7 @@ func NewTeamsNotifier(model *NotificationChannelConfig, t *template.Template) (*
}
return &TeamsNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
@@ -21,7 +20,7 @@ var (
// TelegramNotifier is responsible for sending
// alert notifications to Telegram.
type TelegramNotifier struct {
old_notifiers.NotifierBase
*Base
BotToken string
ChatID string
Message string
@@ -48,7 +47,7 @@ func NewTelegramNotifier(model *NotificationChannelConfig, t *template.Template,
}
return &TelegramNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -10,7 +10,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
@@ -23,7 +22,7 @@ var (
// ThreemaNotifier is responsible for sending
// alert notifications to Threema.
type ThreemaNotifier struct {
old_notifiers.NotifierBase
*Base
GatewayID string
RecipientID string
APISecret string
@@ -62,7 +61,7 @@ func NewThreemaNotifier(model *NotificationChannelConfig, t *template.Template,
}
return &ThreemaNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/grafana/grafana/pkg/setting"
)
@@ -35,7 +34,7 @@ func NewVictoropsNotifier(model *NotificationChannelConfig, t *template.Template
}
return &VictoropsNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
@@ -53,7 +52,7 @@ func NewVictoropsNotifier(model *NotificationChannelConfig, t *template.Template
// and handles notification process by formatting POST body according to
// Victorops specifications (http://victorops.force.com/knowledgebase/articles/Integration/Alert-Ingestion-API-Documentation/)
type VictoropsNotifier struct {
old_notifiers.NotifierBase
*Base
URL string
MessageType string
log log.Logger
@@ -7,7 +7,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
@@ -17,7 +16,7 @@ import (
// WebhookNotifier is responsible for sending
// alert notifications as webhooks.
type WebhookNotifier struct {
old_notifiers.NotifierBase
*Base
URL string
User string
Password string
@@ -39,7 +38,7 @@ func NewWebHookNotifier(model *NotificationChannelConfig, t *template.Template,
return nil, receiverInitError{Cfg: *model, Reason: "could not find url property in settings"}
}
return &WebhookNotifier{
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
Base: NewBase(&models.AlertNotification{
Uid: model.UID,
Name: model.Name,
Type: model.Type,
+43 -17
View File
@@ -24,6 +24,7 @@ var (
)
type TestReceiversResult struct {
Alert types.Alert
Receivers []TestReceiverResult
NotifedAt time.Time
}
@@ -61,19 +62,7 @@ func (e ReceiverTimeoutError) Error() string {
func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigParams) (*TestReceiversResult, error) {
// now represents the start time of the test
now := time.Now()
testAlert := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
model.LabelName("alertname"): "TestAlert",
model.LabelName("instance"): "Grafana",
},
Annotations: model.LabelSet{
model.LabelName("summary"): "Notification test",
},
StartsAt: now,
},
UpdatedAt: now,
}
testAlert := newTestAlert(c, now, now)
// we must set a group key that is unique per test as some receivers use this key to deduplicate alerts
ctx = notify.WithGroupKey(ctx, testAlert.Labels.String()+now.String())
@@ -97,7 +86,7 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
Error error
}
newTestReceiversResult := func(results []result, notifiedAt time.Time) *TestReceiversResult {
newTestReceiversResult := func(alert types.Alert, results []result, notifiedAt time.Time) *TestReceiversResult {
m := make(map[string]TestReceiverResult)
for _, receiver := range c.Receivers {
// set up the result for this receiver
@@ -122,6 +111,7 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
m[next.ReceiverName] = tmp
}
v := new(TestReceiversResult)
v.Alert = alert
v.Receivers = make([]TestReceiverResult, 0, len(c.Receivers))
v.NotifedAt = notifiedAt
for _, next := range m {
@@ -165,7 +155,7 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
}
if len(jobs) == 0 {
return newTestReceiversResult(invalid, now), nil
return newTestReceiversResult(testAlert, invalid, now), nil
}
numWorkers := maxTestReceiversWorkers
@@ -188,7 +178,7 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
Config: next.Config,
ReceiverName: next.ReceiverName,
}
if _, err := next.Notifier.Notify(ctx, testAlert); err != nil {
if _, err := next.Notifier.Notify(ctx, &testAlert); err != nil {
v.Error = err
}
resultCh <- v
@@ -204,7 +194,43 @@ func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestRecei
results = append(results, next)
}
return newTestReceiversResult(append(invalid, results...), now), nil
return newTestReceiversResult(testAlert, append(invalid, results...), now), nil
}
func newTestAlert(c apimodels.TestReceiversConfigParams, startsAt, updatedAt time.Time) types.Alert {
var (
defaultAnnotations = model.LabelSet{
"summary": "Notification test",
}
defaultLabels = model.LabelSet{
"alertname": "TestAlert",
"instance": "Grafana",
}
)
alert := types.Alert{
Alert: model.Alert{
Labels: defaultLabels,
Annotations: defaultAnnotations,
StartsAt: startsAt,
},
UpdatedAt: updatedAt,
}
if c.Alert != nil {
if c.Alert.Annotations != nil {
for k, v := range c.Alert.Annotations {
alert.Annotations[k] = v
}
}
if c.Alert.Labels != nil {
for k, v := range c.Alert.Labels {
alert.Labels[k] = v
}
}
}
return alert
}
func processNotifierError(config *apimodels.PostableGrafanaReceiver, err error) error {
@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"sort"
"strings"
@@ -129,21 +128,6 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, orgID int64, channelUid
return err
}
// Grafana accepts any type of string as a URL for the Slack notification channel.
// However, the Alertmanager will fail if provided with an invalid URL we have two options at this point:
// Either we fail the migration or remove the URL, we've chosen the latter and assume that the notification
// channel was broken to begin with.
if c.Type == "slack" {
u, ok := decryptedSecureSettings["url"]
if ok {
_, err := url.Parse(u)
if err != nil {
m.mg.Logger.Warn("slack notification channel had invalid URL, removing", "name", c.Name, "uid", c.Uid, "org", c.OrgID)
delete(decryptedSecureSettings, "url")
}
}
}
portedChannels = append(portedChannels, &PostableGrafanaReceiver{
UID: uid,
Name: c.Name,
@@ -1,119 +0,0 @@
package ualert
import (
"fmt"
"math/rand"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/util"
)
func Test_makeReceiverAndRoute(t *testing.T) {
emptyMigration := func() *migration {
return &migration{
mg: &migrator.Migrator{
Logger: log.New("test"),
},
migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}),
portedChannelGroupsPerOrg: make(map[int64]map[string]string),
seenChannelUIDs: make(map[string]struct{}),
}
}
generateChannel := func(channelType string, settings map[string]interface{}, secureSettings map[string]string) *notificationChannel {
uid := util.GenerateShortUID()
return &notificationChannel{
ID: rand.Int63(),
OrgID: rand.Int63(),
Uid: uid,
Name: fmt.Sprintf("Test-%s", uid),
Type: channelType,
DisableResolveMessage: rand.Int63()%2 == 0,
IsDefault: rand.Int63()%2 == 0,
Settings: simplejson.NewFromAny(settings),
SecureSettings: GetEncryptedJsonData(secureSettings),
}
}
t.Run("Slack channel is migrated", func(t *testing.T) {
t.Run("url is removed if it is invalid (secure settings)", func(t *testing.T) {
secureSettings := map[string]string{
"url": invalidUri,
"token": util.GenerateShortUID(),
}
settings := map[string]interface{}{
"test": "data",
"some_map": map[string]interface{}{
"test": rand.Int63(),
},
}
channel := generateChannel("slack", settings, secureSettings)
channelsUid := []interface{}{
channel.Uid,
}
defaultChannels := make([]*notificationChannel, 0)
allChannels := map[interface{}]*notificationChannel{
channel.Uid: channel,
}
apiReceiver, _, err := emptyMigration().makeReceiverAndRoute(util.GenerateShortUID(), channel.OrgID, channelsUid, defaultChannels, allChannels)
require.NoError(t, err)
require.Len(t, apiReceiver.GrafanaManagedReceivers, 1)
receiver := apiReceiver.GrafanaManagedReceivers[0]
require.NotContains(t, receiver.SecureSettings, "url")
require.Contains(t, receiver.SecureSettings, "token")
require.Equal(t, secureSettings["token"], receiver.SecureSettings["token"])
actualSettings, err := receiver.Settings.Map()
require.NoError(t, err)
require.Equal(t, settings, actualSettings)
})
t.Run("url is removed if it is invalid (settings)", func(t *testing.T) {
secureSettings := map[string]string{
"token": util.GenerateShortUID(),
}
settings := map[string]interface{}{
"url": invalidUri,
"test": "data",
"some_map": map[string]interface{}{
"test": rand.Int63(),
},
}
channel := generateChannel("slack", settings, secureSettings)
channelsUid := []interface{}{
channel.Uid,
}
defaultChannels := make([]*notificationChannel, 0)
allChannels := map[interface{}]*notificationChannel{
channel.Uid: channel,
}
apiReceiver, _, err := emptyMigration().makeReceiverAndRoute(util.GenerateShortUID(), channel.OrgID, channelsUid, defaultChannels, allChannels)
require.NoError(t, err)
require.Len(t, apiReceiver.GrafanaManagedReceivers, 1)
receiver := apiReceiver.GrafanaManagedReceivers[0]
require.NotContains(t, receiver.SecureSettings, "url")
require.Contains(t, receiver.SecureSettings, "token")
require.Equal(t, secureSettings["token"], receiver.SecureSettings["token"])
actualSettings, err := receiver.Settings.Map()
require.NoError(t, err)
delete(settings, "url")
require.Equal(t, settings, actualSettings)
})
})
}
const invalidUri = "�6�M��)uk譹1(�h`$�o�N>mĕ����cS2�dh![ę� ���`csB�!��OSxP�{�"
@@ -0,0 +1,23 @@
package ualert
import (
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
// newTestMigration generates an empty migration to use in tests.
func newTestMigration(t *testing.T) *migration {
t.Helper()
return &migration{
mg: &migrator.Migrator{
Logger: log.New("test"),
},
migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}),
portedChannelGroupsPerOrg: make(map[int64]map[string]string),
seenChannelUIDs: make(map[string]struct{}),
}
}
+115 -14
View File
@@ -1,6 +1,8 @@
package ualert
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
@@ -9,11 +11,12 @@ import (
"strings"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/util"
pb "github.com/prometheus/alertmanager/silence/silencepb"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
const GENERAL_FOLDER = "General Alerting"
@@ -217,6 +220,7 @@ func (m *migration) SQL(dialect migrator.Dialect) string {
return "code migration"
}
//nolint: gocyclo
func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
m.sess = sess
m.mg = mg
@@ -377,7 +381,24 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
return err
}
if err := m.writeAlertmanagerConfig(orgID, amConfig, allChannelsPerOrg[orgID]); err != nil {
// No channels, hence don't require Alertmanager config - skip it.
if len(allChannelsPerOrg[orgID]) == 0 {
m.mg.Logger.Info("alert migration: no notification channel found, skipping Alertmanager config")
continue
}
// Encrypt the secure settings before we continue.
if err := amConfig.EncryptSecureSettings(); err != nil {
return err
}
// Validate the alertmanager configuration produced, this gives a chance to catch bad configuration at migration time.
// Validation between legacy and unified alerting can be different (e.g. due to bug fixes) so this would fail the migration in that case.
if err := m.validateAlertmanagerConfig(orgID, amConfig); err != nil {
return err
}
if err := m.writeAlertmanagerConfig(orgID, amConfig); err != nil {
return err
}
@@ -389,22 +410,13 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
return nil
}
func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel) error {
if len(allChannels) == 0 {
// No channels, hence don't require Alertmanager config.
m.mg.Logger.Info("alert migration: no notification channel found, skipping Alertmanager config")
return nil
}
if err := amConfig.EncryptSecureSettings(); err != nil {
return err
}
func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserConfig) error {
rawAmConfig, err := json.Marshal(amConfig)
if err != nil {
return err
}
// TODO: should we apply the config here? Because Alertmanager can take upto 1 min to pick it up.
// We don't need to apply the configuration, given the multi org alertmanager will do an initial sync before the server is ready.
_, err = m.sess.Insert(AlertConfiguration{
AlertmanagerConfiguration: string(rawAmConfig),
// Since we are migration for a snapshot of the code, it is always going to migrate to
@@ -419,6 +431,95 @@ func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserC
return nil
}
// validateAlertmanagerConfig validates the alertmanager configuration produced by the migration against the receivers.
func (m *migration) validateAlertmanagerConfig(orgID int64, config *PostableUserConfig) error {
for _, r := range config.AlertmanagerConfig.Receivers {
for _, gr := range r.GrafanaManagedReceivers {
// First, let's decode the secure settings - given they're stored as base64.
secureSettings := make(map[string][]byte, len(gr.SecureSettings))
for k, v := range gr.SecureSettings {
d, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return err
}
secureSettings[k] = d
}
var (
cfg = &channels.NotificationChannelConfig{
UID: gr.UID,
OrgID: orgID,
Name: gr.Name,
Type: gr.Type,
DisableResolveMessage: gr.DisableResolveMessage,
Settings: gr.Settings,
SecureSettings: secureSettings,
}
err error
)
// decryptFunc represents the legacy way of decrypting data. Before the migration, we don't need any new way,
// given that the previous alerting will never support it.
decryptFunc := func(_ context.Context, sjd map[string][]byte, key string, fallback string, secret string) string {
if value, ok := sjd[key]; ok {
decryptedData, err := util.Decrypt(value, secret)
if err != nil {
m.mg.Logger.Warn("unable to decrypt key '%s' for %s receiver with uid %s, returning fallback.", key, gr.Type, gr.UID)
return fallback
}
return string(decryptedData)
}
return fallback
}
switch gr.Type {
case "email":
_, err = channels.NewEmailNotifier(cfg, nil) // Email notifier already has a default template.
case "pagerduty":
_, err = channels.NewPagerdutyNotifier(cfg, nil, decryptFunc)
case "pushover":
_, err = channels.NewPushoverNotifier(cfg, nil, decryptFunc)
case "slack":
_, err = channels.NewSlackNotifier(cfg, nil, decryptFunc)
case "telegram":
_, err = channels.NewTelegramNotifier(cfg, nil, decryptFunc)
case "victorops":
_, err = channels.NewVictoropsNotifier(cfg, nil)
case "teams":
_, err = channels.NewTeamsNotifier(cfg, nil)
case "dingding":
_, err = channels.NewDingDingNotifier(cfg, nil)
case "kafka":
_, err = channels.NewKafkaNotifier(cfg, nil)
case "webhook":
_, err = channels.NewWebHookNotifier(cfg, nil, decryptFunc)
case "sensugo":
_, err = channels.NewSensuGoNotifier(cfg, nil, decryptFunc)
case "discord":
_, err = channels.NewDiscordNotifier(cfg, nil)
case "googlechat":
_, err = channels.NewGoogleChatNotifier(cfg, nil)
case "LINE":
_, err = channels.NewLineNotifier(cfg, nil, decryptFunc)
case "threema":
_, err = channels.NewThreemaNotifier(cfg, nil, decryptFunc)
case "opsgenie":
_, err = channels.NewOpsgenieNotifier(cfg, nil, decryptFunc)
case "prometheus-alertmanager":
_, err = channels.NewAlertmanagerNotifier(cfg, nil, decryptFunc)
default:
return fmt.Errorf("notifier %s is not supported", gr.Type)
}
if err != nil {
return err
}
}
}
return nil
}
type AlertConfiguration struct {
ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
@@ -0,0 +1,90 @@
package ualert
import (
"errors"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/require"
)
func Test_validateAlertmanagerConfig(t *testing.T) {
tc := []struct {
name string
receivers []*PostableGrafanaReceiver
err error
}{
{
name: "when a slack receiver does not have a valid URL - it should error",
receivers: []*PostableGrafanaReceiver{
{
UID: util.GenerateShortUID(),
Name: "SlackWithBadURL",
Type: "slack",
Settings: simplejson.NewFromAny(map[string]interface{}{}),
SecureSettings: map[string]string{"url": invalidUri},
},
},
err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": invalid URL %q: parse %q: net/url: invalid control character in URL", invalidUri, invalidUri),
},
{
name: "when a slack receiver has an invalid recipient - it should error",
receivers: []*PostableGrafanaReceiver{
{
UID: util.GenerateShortUID(),
Name: "SlackWithBadRecipient",
Type: "slack",
Settings: simplejson.NewFromAny(map[string]interface{}{"recipient": "this-doesnt-pass"}),
SecureSettings: map[string]string{"url": "http://webhook.slack.com/myuser"},
},
},
err: errors.New("failed to validate receiver \"SlackWithBadRecipient\" of type \"slack\": recipient on invalid format: \"this-doesnt-pass\""),
},
{
name: "when the configuration is valid - it should not error",
receivers: []*PostableGrafanaReceiver{
{
UID: util.GenerateShortUID(),
Name: "SlackWithBadURL",
Type: "slack",
Settings: simplejson.NewFromAny(map[string]interface{}{"recipient": "#a-good-channel"}),
SecureSettings: map[string]string{"url": "http://webhook.slack.com/myuser"},
},
},
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
mg := newTestMigration(t)
orgID := int64(1)
config := configFromReceivers(t, tt.receivers)
require.NoError(t, config.EncryptSecureSettings()) // make sure we encrypt the settings
err := mg.validateAlertmanagerConfig(orgID, config)
if tt.err != nil {
require.Error(t, err)
require.EqualError(t, err, tt.err.Error())
} else {
require.NoError(t, err)
}
})
}
}
func configFromReceivers(t *testing.T, receivers []*PostableGrafanaReceiver) *PostableUserConfig {
t.Helper()
return &PostableUserConfig{
AlertmanagerConfig: PostableApiAlertingConfig{
Receivers: []*PostableApiReceiver{
{GrafanaManagedReceivers: receivers},
},
},
}
}
const invalidUri = "�6�M��)uk譹1(�h`$�o�N>mĕ����cS2�dh![ę� ���`csB�!��OSxP�{�"
@@ -92,14 +92,14 @@ func TestTestReceivers(t *testing.T) {
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid":"",
"name":"receiver-1",
"type":"email",
"disableResolveMessage":false,
"settings":{
"uid": "",
"name": "receiver-1",
"type": "email",
"disableResolveMessage": false,
"settings": {
"addresses":"example@email.com"
},
"secureFields":{}
"secureFields": {}
}
]
}]
@@ -109,22 +109,39 @@ func TestTestReceivers(t *testing.T) {
require.NoError(t, err)
})
var result apimodels.TestReceiversResult
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 1)
require.Len(t, result.Receivers[0].Configs, 1)
require.Equal(t, apimodels.TestReceiversResult{
Receivers: []apimodels.TestReceiverResult{{
Name: "receiver-1",
Configs: []apimodels.TestReceiverConfigResult{{
Name: "receiver-1",
UID: result.Receivers[0].Configs[0].UID,
Status: "ok",
}},
}},
NotifedAt: result.NotifedAt,
}, result)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"summary": "Notification test"
},
"labels": {
"alertname": "TestAlert",
"instance": "Grafana"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "ok"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
require.Len(t, mockEmails.emails, 1)
require.Equal(t, []string{"example@email.com"}, mockEmails.emails[0].To)
@@ -160,38 +177,54 @@ func TestTestReceivers(t *testing.T) {
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid":"",
"name":"receiver-1",
"type":"email",
"disableResolveMessage":false,
"settings":{},
"secureFields":{}
"uid": "",
"name": "receiver-1",
"type": "email",
"disableResolveMessage": false,
"settings": {},
"secureFields": {}
}
]
}]
}`, http.StatusBadRequest)
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 1)
require.Len(t, result.Receivers[0].Configs, 1)
require.Equal(t, apimodels.TestReceiversResult{
Receivers: []apimodels.TestReceiverResult{{
Name: "receiver-1",
Configs: []apimodels.TestReceiverConfigResult{{
Name: "receiver-1",
UID: result.Receivers[0].Configs[0].UID,
Status: "failed",
Error: "the receiver is invalid: failed to validate receiver \"receiver-1\" of type \"email\": could not find addresses in settings",
}},
}},
NotifedAt: result.NotifedAt,
}, result)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"summary": "Notification test"
},
"labels": {
"alertname": "TestAlert",
"instance": "Grafana"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "failed",
"error": "the receiver is invalid: failed to validate receiver \"receiver-1\" of type \"email\": could not find addresses in settings"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
})
t.Run("assert timed out receiver returns 408 Request Timeout", func(t *testing.T) {
@@ -225,14 +258,14 @@ func TestTestReceivers(t *testing.T) {
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid":"",
"name":"receiver-1",
"type":"email",
"disableResolveMessage":false,
"settings":{
"uid": "",
"name": "receiver-1",
"type": "email",
"disableResolveMessage": false,
"settings": {
"addresses":"example@email.com"
},
"secureFields":{}
"secureFields": {}
}
]
}]
@@ -248,23 +281,40 @@ func TestTestReceivers(t *testing.T) {
})
require.Equal(t, http.StatusRequestTimeout, resp.StatusCode)
var result apimodels.TestReceiversResult
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 1)
require.Len(t, result.Receivers[0].Configs, 1)
require.Equal(t, apimodels.TestReceiversResult{
Receivers: []apimodels.TestReceiverResult{{
Name: "receiver-1",
Configs: []apimodels.TestReceiverConfigResult{{
Name: "receiver-1",
UID: result.Receivers[0].Configs[0].UID,
Status: "failed",
Error: "the receiver timed out: context deadline exceeded",
}},
}},
NotifedAt: result.NotifedAt,
}, result)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"summary": "Notification test"
},
"labels": {
"alertname": "TestAlert",
"instance": "Grafana"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "failed",
"error": "the receiver timed out: context deadline exceeded"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
})
t.Run("assert multiple different errors returns 207 Multi Status", func(t *testing.T) {
@@ -298,26 +348,26 @@ func TestTestReceivers(t *testing.T) {
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid":"",
"name":"receiver-1",
"type":"email",
"disableResolveMessage":false,
"settings":{},
"secureFields":{}
"uid": "",
"name": "receiver-1",
"type": "email",
"disableResolveMessage": false,
"settings": {},
"secureFields": {}
}
]
}, {
"name":"receiver-2",
"grafana_managed_receiver_configs": [
{
"uid":"",
"name":"receiver-2",
"type":"email",
"disableResolveMessage":false,
"settings":{
"uid": "",
"name": "receiver-2",
"type": "email",
"disableResolveMessage": false,
"settings": {
"addresses":"example@email.com"
},
"secureFields":{}
"secureFields": {}
}
]
}]
@@ -333,32 +383,329 @@ func TestTestReceivers(t *testing.T) {
})
require.Equal(t, http.StatusMultiStatus, resp.StatusCode)
var result apimodels.TestReceiversResult
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 2)
require.Len(t, result.Receivers[0].Configs, 1)
require.Len(t, result.Receivers[1].Configs, 1)
require.Equal(t, apimodels.TestReceiversResult{
Receivers: []apimodels.TestReceiverResult{{
Name: "receiver-1",
Configs: []apimodels.TestReceiverConfigResult{{
Name: "receiver-1",
UID: result.Receivers[0].Configs[0].UID,
Status: "failed",
Error: "the receiver is invalid: failed to validate receiver \"receiver-1\" of type \"email\": could not find addresses in settings",
}},
}, {
Name: "receiver-2",
Configs: []apimodels.TestReceiverConfigResult{{
Name: "receiver-2",
UID: result.Receivers[1].Configs[0].UID,
Status: "failed",
Error: "the receiver timed out: context deadline exceeded",
}},
}},
NotifedAt: result.NotifedAt,
}, result)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"summary": "Notification test"
},
"labels": {
"alertname": "TestAlert",
"instance": "Grafana"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "failed",
"error": "the receiver is invalid: failed to validate receiver \"receiver-1\" of type \"email\": could not find addresses in settings"
}
]
}, {
"name":"receiver-2",
"grafana_managed_receiver_configs": [
{
"name": "receiver-2",
"uid": "%s",
"status": "failed",
"error": "the receiver timed out: context deadline exceeded"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.Receivers[1].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
})
}
func TestTestReceiversAlertCustomization(t *testing.T) {
t.Run("assert custom annotations and labels are sent", func(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
store.Bus = bus.GetBus()
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Login: "grafana",
Password: "password",
})
oldEmailBus := bus.GetHandlerCtx("SendEmailCommandSync")
mockEmails := &mockEmailHandler{}
bus.AddHandlerCtx("", mockEmails.sendEmailCommandHandlerSync)
t.Cleanup(func() {
bus.AddHandlerCtx("", oldEmailBus)
})
testReceiversURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/receivers/test", grafanaListedAddr)
// nolint
resp := postRequest(t, testReceiversURL, `{
"alert": {
"annotations": {
"annotation1": "value1"
},
"labels": {
"label1": "value1"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid":"",
"name":"receiver-1",
"type":"email",
"disableResolveMessage":false,
"settings":{
"addresses":"example@email.com"
},
"secureFields":{}
}
]
}]
}`, http.StatusOK)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 1)
require.Len(t, result.Receivers[0].Configs, 1)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"annotation1": "value1",
"summary": "Notification test"
},
"labels": {
"alertname": "TestAlert",
"instance": "Grafana",
"label1": "value1"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "ok"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
require.Len(t, mockEmails.emails, 1)
require.Equal(t, []string{"example@email.com"}, mockEmails.emails[0].To)
})
t.Run("assert custom annotations can replace default annotations", func(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
store.Bus = bus.GetBus()
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Login: "grafana",
Password: "password",
})
oldEmailBus := bus.GetHandlerCtx("SendEmailCommandSync")
mockEmails := &mockEmailHandler{}
bus.AddHandlerCtx("", mockEmails.sendEmailCommandHandlerSync)
t.Cleanup(func() {
bus.AddHandlerCtx("", oldEmailBus)
})
testReceiversURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/receivers/test", grafanaListedAddr)
// nolint
resp := postRequest(t, testReceiversURL, `{
"alert": {
"annotations": {
"summary": "This is a custom annotation"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid":"",
"name":"receiver-1",
"type":"email",
"disableResolveMessage":false,
"settings":{
"addresses":"example@email.com"
},
"secureFields":{}
}
]
}]
}`, http.StatusOK)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 1)
require.Len(t, result.Receivers[0].Configs, 1)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"summary": "This is a custom annotation"
},
"labels": {
"alertname": "TestAlert",
"instance": "Grafana"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "ok"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
require.Len(t, mockEmails.emails, 1)
require.Equal(t, []string{"example@email.com"}, mockEmails.emails[0].To)
})
t.Run("assert custom labels can replace default label", func(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
store.Bus = bus.GetBus()
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Login: "grafana",
Password: "password",
})
oldEmailBus := bus.GetHandlerCtx("SendEmailCommandSync")
mockEmails := &mockEmailHandler{}
bus.AddHandlerCtx("", mockEmails.sendEmailCommandHandlerSync)
t.Cleanup(func() {
bus.AddHandlerCtx("", oldEmailBus)
})
testReceiversURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/receivers/test", grafanaListedAddr)
// nolint
resp := postRequest(t, testReceiversURL, `{
"alert": {
"labels": {
"alertname": "This is a custom label"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid":"",
"name":"receiver-1",
"type":"email",
"disableResolveMessage":false,
"settings":{
"addresses":"example@email.com"
},
"secureFields":{}
}
]
}]
}`, http.StatusOK)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 1)
require.Len(t, result.Receivers[0].Configs, 1)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"summary": "Notification test"
},
"labels": {
"alertname": "This is a custom label",
"instance": "Grafana"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "ok"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
require.Len(t, mockEmails.emails, 1)
require.Equal(t, []string{"example@email.com"}, mockEmails.emails[0].To)
})
}
+19 -10
View File
@@ -181,9 +181,9 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
if err != nil {
plog.Error("Exemplar query", query.Expr, "failed with", err)
result.Responses[query.RefId] = backend.DataResponse{Error: err}
continue
} else {
response[ExemplarQueryType] = exemplarResponse
}
response[ExemplarQueryType] = exemplarResponse
}
frames, err := parseResponse(response, query)
@@ -403,19 +403,28 @@ func matrixToDataFrames(matrix model.Matrix, query *PrometheusQuery) data.Frames
for _, v := range matrix {
tags := make(map[string]string, len(v.Metric))
timeVector := make([]time.Time, 0, len(v.Values))
values := make([]float64, 0, len(v.Values))
for k, v := range v.Metric {
tags[string(k)] = string(v)
}
for _, k := range v.Values {
timeVector = append(timeVector, time.Unix(k.Timestamp.Unix(), 0).UTC())
values = append(values, float64(k.Value))
timeField := data.NewFieldFromFieldType(data.FieldTypeTime, len(v.Values))
valueField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(v.Values))
for i, k := range v.Values {
timeField.Set(i, time.Unix(k.Timestamp.Unix(), 0).UTC())
value := float64(k.Value)
if !math.IsNaN(value) {
valueField.Set(i, &value)
}
}
name := formatLegend(v.Metric, query)
frame := data.NewFrame(name,
data.NewField("Time", nil, timeVector),
data.NewField("Value", tags, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name}))
timeField.Name = data.TimeSeriesTimeFieldName
valueField.Name = data.TimeSeriesValueFieldName
valueField.Config = &data.FieldConfig{DisplayNameFromDS: name}
valueField.Labels = tags
frame := data.NewFrame(name, timeField, valueField)
frame.Meta = &data.FrameMeta{
Custom: map[string]string{
"resultType": "matrix",
+22
View File
@@ -1,6 +1,7 @@
package prometheus
import (
"math"
"testing"
"time"
@@ -440,6 +441,27 @@ func TestPrometheus_parseResponse(t *testing.T) {
require.Equal(t, "UTC", testValue.(time.Time).Location().String())
})
t.Run("matrix response with NaN value should be changed to null", func(t *testing.T) {
value := make(map[PrometheusQueryType]interface{})
value[RangeQueryType] = p.Matrix{
&p.SampleStream{
Metric: p.Metric{"app": "Application"},
Values: []p.SamplePair{
{Value: p.SampleValue(math.NaN()), Timestamp: 1000},
},
},
}
query := &PrometheusQuery{
LegendFormat: "",
}
res, err := parseResponse(value, query)
require.NoError(t, err)
var nilPointer *float64
require.Equal(t, res[0].Fields[1].Name, "Value")
require.Equal(t, res[0].Fields[1].At(0), nilPointer)
})
t.Run("vector response should be parsed normally", func(t *testing.T) {
value := make(map[PrometheusQueryType]interface{})
value[RangeQueryType] = p.Vector{
@@ -68,6 +68,7 @@ export const TagFilter: FC<Props> = ({
const value = tags.map((tag) => ({ value: tag, label: tag, count: 0 }));
const selectOptions = {
allowCreateWhileLoading: true,
allowCustomValue,
formatCreateLabel,
defaultOptions: true,
@@ -1,10 +1,11 @@
import React, { FC, ReactNode, useCallback, useEffect, useState } from 'react';
import React, { FC, ReactNode, useCallback, useEffect, useState, useRef } from 'react';
import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Counter, Icon, useStyles2 } from '@grafana/ui';
import { PANEL_EDITOR_UI_STATE_STORAGE_KEY } from './state/reducers';
import { useLocalStorage } from 'react-use';
import { selectors } from '@grafana/e2e-selectors';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
export interface OptionsPaneCategoryProps {
id: string;
@@ -18,28 +19,47 @@ export interface OptionsPaneCategoryProps {
children: ReactNode;
}
const CATEGORY_PARAM_NAME = 'showCategory';
export const OptionsPaneCategory: FC<OptionsPaneCategoryProps> = React.memo(
({ id, title, children, forceOpen, isOpenDefault, renderTitle, className, itemsCount, isNested = false }) => {
const initialIsExpanded = isOpenDefault !== false;
const [savedState, setSavedState] = useLocalStorage(getOptionGroupStorageKey(id), {
isExpanded: initialIsExpanded,
});
// `savedState` can be undefined by typescript, so we have to handle that case
const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? initialIsExpanded);
const styles = useStyles2(getStyles);
const [queryParams, updateQueryParams] = useQueryParams();
const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? initialIsExpanded);
const manualClickTime = useRef(0);
const ref = useRef<HTMLDivElement>(null);
const isOpenFromUrl = queryParams[CATEGORY_PARAM_NAME] === id;
useEffect(() => {
if (!isExpanded && forceOpen && forceOpen > 0) {
setIsExpanded(true);
if (manualClickTime.current) {
// ignore changes since the click handled the expected behavior
if (Date.now() - manualClickTime.current < 200) {
return;
}
}
}, [forceOpen, isExpanded]);
if (isOpenFromUrl || forceOpen) {
if (!isExpanded) {
setIsExpanded(true);
}
if (isOpenFromUrl) {
ref.current?.scrollIntoView();
}
}
}, [forceOpen, isExpanded, isOpenFromUrl]);
const onToggle = useCallback(() => {
manualClickTime.current = Date.now();
updateQueryParams({
[CATEGORY_PARAM_NAME]: isExpanded ? undefined : id,
});
setSavedState({ isExpanded: !isExpanded });
setIsExpanded(!isExpanded);
}, [setSavedState, setIsExpanded, isExpanded]);
}, [setSavedState, setIsExpanded, updateQueryParams, isExpanded, id]);
if (!renderTitle) {
renderTitle = function defaultTitle(isExpanded: boolean) {
@@ -78,6 +98,7 @@ export const OptionsPaneCategory: FC<OptionsPaneCategoryProps> = React.memo(
className={boxStyles}
data-testid="options-category"
aria-label={selectors.components.OptionsGroup.group(id)}
ref={ref}
>
<div className={headerStyles} onClick={onToggle} aria-label={selectors.components.OptionsGroup.toggle(id)}>
<div className={cx(styles.toggle, 'editor-options-group-toggle')}>
@@ -21,6 +21,12 @@ standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
const mockStore = configureMockStore<any, any>();
const OptionsPaneSelector = selectors.components.PanelEditor.OptionsPane;
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom') as any),
useLocation: () => ({
pathname: 'localhost:3000/example/path',
}),
}));
class OptionsPaneOptionsTestScenario {
onFieldConfigsChange = jest.fn();
@@ -309,12 +309,17 @@ export class PanelChrome extends PureComponent<Props, State> {
this.props.panel.updateFieldConfig(config);
};
onPanelError = (message: string) => {
if (this.state.errorMessage !== message) {
this.setState({ errorMessage: message });
onPanelError = (error: Error) => {
const errorMessage = error.message || DEFAULT_PLUGIN_ERROR;
if (this.state.errorMessage !== errorMessage) {
this.setState({ errorMessage });
}
};
onPanelErrorRecover = () => {
this.setState({ errorMessage: undefined });
};
onAnnotationCreate = async (event: AnnotationEventUIModel) => {
const isRegion = event.from !== event.to;
const anno = {
@@ -458,7 +463,7 @@ export class PanelChrome extends PureComponent<Props, State> {
}
render() {
const { dashboard, panel, isViewing, isEditing, width, height } = this.props;
const { dashboard, panel, isViewing, isEditing, width, height, plugin } = this.props;
const { errorMessage, data } = this.state;
const { transparent } = panel;
@@ -489,10 +494,13 @@ export class PanelChrome extends PureComponent<Props, State> {
alertState={alertState}
data={data}
/>
<ErrorBoundary>
<ErrorBoundary
dependencies={[data, plugin, panel.getOptions()]}
onError={this.onPanelError}
onRecover={this.onPanelErrorRecover}
>
{({ error }) => {
if (error) {
this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR);
return null;
}
return this.renderPanel(width, height);
@@ -22,6 +22,7 @@ import { Logs } from './Logs';
import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
import { LiveTailControls } from './useLiveTailControls';
import { getFieldLinksForExplore } from './utils/links';
import { config } from 'app/core/config';
interface LogsContainerProps extends PropsFromRedux {
width: number;
@@ -150,7 +151,11 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
getFieldLinks={this.getFieldLinks}
addResultsToCache={() => addResultsToCache(exploreId)}
clearCache={() => clearCache(exploreId)}
loadingLogsVolumeAvailable={hasLogsVolumeSupport(datasourceInstance) && !!logsVolumeDataProvider}
loadingLogsVolumeAvailable={
hasLogsVolumeSupport(datasourceInstance) &&
!!logsVolumeDataProvider &&
!config.featureToggles.autoLoadFullRangeLogsVolume
}
onClickLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
/>
</Collapse>
@@ -3,6 +3,19 @@ import { noop } from 'lodash';
import { shallow } from 'enzyme';
import { SecondaryActions } from './SecondaryActions';
import { config } from '@grafana/runtime';
jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object),
config: {
...((jest.requireActual('@grafana/runtime') as unknown) as any).config,
featureToggles: {
fullRangeLogsVolume: true,
autoLoadFullRangeLogsVolume: false,
},
},
}));
const addQueryRowButtonSelector = '[aria-label="Add row button"]';
const richHistoryButtonSelector = '[aria-label="Rich history button"]';
const queryInspectorButtonSelector = '[aria-label="Query inspector button"]';
@@ -66,4 +79,8 @@ describe('SecondaryActions', () => {
wrapper.find(queryInspectorButtonSelector).simulate('click');
expect(onClickQueryInspector).toBeCalled();
});
it('does not render load logs volume button when auto loading is enabled', () => {
config.featureToggles.autoLoadFullRangeLogsVolume = true;
});
});
@@ -33,6 +33,7 @@ import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { configureStore } from '../../../store/configureStore';
import { setTimeSrv } from '../../dashboard/services/TimeSrv';
import Mock = jest.Mock;
import { config } from '@grafana/runtime';
jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as object),
@@ -40,6 +41,7 @@ jest.mock('@grafana/runtime', () => ({
...((jest.requireActual('@grafana/runtime') as unknown) as any).config,
featureToggles: {
fullRangeLogsVolume: true,
autoLoadFullRangeLogsVolume: false,
},
},
}));
@@ -322,6 +324,7 @@ describe('reducer', () => {
describe('logs volume', () => {
let dispatch: ThunkDispatch,
getState: () => StoreState,
unsubscribes: Function[],
mockLogsVolumeDataProvider: () => Observable<DataQueryResponse>;
beforeEach(() => {
@@ -352,11 +355,9 @@ describe('reducer', () => {
dispatch = store.dispatch;
getState = store.getState;
});
it('should cancel any unfinished logs volume queries', async () => {
setupQueryResponse(getState());
let unsubscribes: Function[] = [];
unsubscribes = [];
mockLogsVolumeDataProvider = () => {
return ({
@@ -369,7 +370,9 @@ describe('reducer', () => {
},
} as unknown) as Observable<DataQueryResponse>;
};
});
it('should cancel any unfinished logs volume queries', async () => {
await dispatch(runQueries(ExploreId.left));
// no subscriptions created yet
expect(unsubscribes).toHaveLength(0);
@@ -391,5 +394,11 @@ describe('reducer', () => {
expect(unsubscribes[0]).toBeCalled();
expect(unsubscribes[1]).not.toBeCalled();
});
it('should load logs volume after running the query', async () => {
config.featureToggles.autoLoadFullRangeLogsVolume = true;
await dispatch(runQueries(ExploreId.left));
expect(unsubscribes).toHaveLength(1);
});
});
});
@@ -485,6 +485,9 @@ export const runQueries = (
const { logsVolumeData, absoluteRange } = getState().explore[exploreId]!;
if (!canReuseLogsVolumeData(logsVolumeData, queries, absoluteRange)) {
dispatch(cleanLogsVolumeAction({ exploreId }));
if (config.featureToggles.autoLoadFullRangeLogsVolume) {
dispatch(loadLogsVolumeData(exploreId));
}
}
} else {
dispatch(
@@ -5,7 +5,7 @@ import { appEvents } from 'app/core/core';
import { useAsync } from 'react-use';
import { getPanelOptionsWithDefaults, OptionDefaults } from '../../dashboard/state/getPanelOptionsWithDefaults';
import { importPanelPlugin } from '../../plugins/importPanelPlugin';
import { useTheme2 } from '@grafana/ui';
import { ErrorBoundaryAlert, useTheme2 } from '@grafana/ui';
const defaultFieldConfig = { defaults: {}, overrides: [] };
@@ -51,24 +51,26 @@ export function PanelRenderer<P extends object = any, F extends object = any>(pr
const PanelComponent = plugin.panel;
return (
<PanelComponent
id={1}
data={dataWithOverrides}
title={title}
timeRange={dataWithOverrides.timeRange}
timeZone={timeZone}
options={optionsWithDefaults!.options}
fieldConfig={localFieldConfig}
transparent={false}
width={width}
height={height}
renderCounter={0}
replaceVariables={(str: string) => str}
onOptionsChange={onOptionsChange}
onFieldConfigChange={setFieldConfig}
onChangeTimeRange={onChangeTimeRange}
eventBus={appEvents}
/>
<ErrorBoundaryAlert dependencies={[plugin, data]}>
<PanelComponent
id={1}
data={dataWithOverrides}
title={title}
timeRange={dataWithOverrides.timeRange}
timeZone={timeZone}
options={optionsWithDefaults!.options}
fieldConfig={localFieldConfig}
transparent={false}
width={width}
height={height}
renderCounter={0}
replaceVariables={(str: string) => str}
onOptionsChange={onOptionsChange}
onFieldConfigChange={setFieldConfig}
onChangeTimeRange={onChangeTimeRange}
eventBus={appEvents}
/>
</ErrorBoundaryAlert>
);
}
@@ -1,4 +1,3 @@
import { LegendDisplayMode } from '@grafana/schema';
import {
ApplyFieldOverrideOptions,
DataTransformerConfig,
@@ -7,7 +6,7 @@ import {
NavModelItem,
PanelData,
} from '@grafana/data';
import { Table, TimeSeries } from '@grafana/ui';
import { Table } from '@grafana/ui';
import { config } from 'app/core/config';
import React, { FC, useMemo, useState } from 'react';
import { useObservable } from 'react-use';
@@ -16,6 +15,7 @@ import { PanelQueryRunner } from '../query/state/PanelQueryRunner';
import { QueryGroupOptions } from 'app/types';
import Page from '../../core/components/Page/Page';
import AutoSizer from 'react-virtualized-auto-sizer';
import { PanelRenderer } from '../panel/components/PanelRenderer';
interface State {
queryRunner: PanelQueryRunner;
@@ -66,12 +66,14 @@ export const TestStuffPage: FC = () => {
{({ width }) => {
return (
<div>
<TimeSeries
<PanelRenderer
title="Hello"
pluginId="timeseries"
width={width}
height={300}
frames={data.series}
legend={{ displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] }}
timeRange={data.timeRange}
data={data}
options={{}}
fieldConfig={{ defaults: {}, overrides: [] }}
timeZone="browser"
/>
<Table data={data.series[0]} width={width} height={300} />
@@ -17,8 +17,6 @@ import { FixedSizeList } from 'react-window';
import { GrafanaTheme } from '@grafana/data';
// Hard limit on labels to render
const MAX_LABEL_COUNT = 10000;
const MAX_VALUE_COUNT = 50000;
const EMPTY_SELECTOR = '{}';
const METRIC_LABEL = '__name__';
const LIST_ITEM_SIZE = 25;
@@ -321,12 +319,6 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
const selectedLabels: string[] = lastUsedLabels;
languageProvider.start().then(() => {
let rawLabels: string[] = languageProvider.getLabelKeys();
// TODO too-many-metrics
if (rawLabels.length > MAX_LABEL_COUNT) {
const error = `Too many labels found (showing only ${MAX_LABEL_COUNT} of ${rawLabels.length})`;
rawLabels = rawLabels.slice(0, MAX_LABEL_COUNT);
this.setState({ error });
}
// Get metrics
this.fetchValues(METRIC_LABEL, EMPTY_SELECTOR);
// Auto-select previously selected labels
@@ -394,11 +386,6 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
this.updateLabelState(name, { loading: false });
return;
}
if (rawValues.length > MAX_VALUE_COUNT) {
const error = `Too many values for ${name} (showing only ${MAX_VALUE_COUNT} of ${rawValues.length})`;
rawValues = rawValues.slice(0, MAX_VALUE_COUNT);
this.setState({ error });
}
const values: FacettableValue[] = [];
const { metricsMetadata } = languageProvider;
for (const labelValue of rawValues) {
@@ -570,6 +570,45 @@ describe('PrometheusDatasource', () => {
});
});
describe('applyTemplateVariables', () => {
it('should call replace function for legendFormat', () => {
const query = {
expr: 'test{job="bar"}',
legendFormat: '$legend',
refId: 'A',
};
const legend = 'baz';
templateSrvStub.replace.mockReturnValue(legend);
const interpolatedQuery = ds.applyTemplateVariables(query, { legend: { text: legend, value: legend } });
expect(interpolatedQuery.legendFormat).toBe(legend);
});
it('should call replace function for expr', () => {
const query = {
expr: 'test{job="$job"}',
refId: 'A',
};
const job = 'bar';
templateSrvStub.replace.mockReturnValue(job);
const interpolatedQuery = ds.applyTemplateVariables(query, { job: { text: job, value: job } });
expect(interpolatedQuery.expr).toBe(job);
});
it('should not call replace function for interval', () => {
const query = {
expr: 'test{job="bar"}',
interval: '$interval',
refId: 'A',
};
const interval = '10s';
templateSrvStub.replace.mockReturnValue(interval);
const interpolatedQuery = ds.applyTemplateVariables(query, { interval: { text: interval, value: interval } });
expect(interpolatedQuery.interval).not.toBe(interval);
});
});
describe('metricFindQuery', () => {
beforeEach(() => {
const query = 'query_result(topk(5,rate(http_request_duration_microseconds_count[$__interval])))';
@@ -894,6 +894,7 @@ export class PrometheusDatasource extends DataSourceWithBackend<PromQuery, PromO
return {
...target,
legendFormat: this.templateSrv.replace(target.legendFormat, variables),
expr: this.templateSrv.replace(target.expr, variables, this.interpolateQueryExpr),
};
}
+11 -12
View File
@@ -13,18 +13,17 @@ export class DebugPanel extends Component<Props> {
render() {
const { options } = this.props;
if (options.mode === DebugMode.Events) {
return <EventBusLoggerPanel eventBus={this.props.eventBus} />;
switch (options.mode) {
case DebugMode.Events:
return <EventBusLoggerPanel eventBus={this.props.eventBus} />;
case DebugMode.Cursor:
return <CursorView eventBus={this.props.eventBus} />;
case DebugMode.State:
return <StateView {...this.props} />;
case DebugMode.ThrowError:
throw new Error('I failed you and for that i am deeply sorry');
default:
return <RenderInfoViewer {...this.props} />;
}
if (options.mode === DebugMode.Cursor) {
return <CursorView eventBus={this.props.eventBus} />;
}
if (options.mode === DebugMode.State) {
return <StateView {...this.props} />;
}
return <RenderInfoViewer {...this.props} />;
}
}
+3 -1
View File
@@ -5,7 +5,7 @@ import { DebugMode, DebugPanelOptions } from './types';
export const plugin = new PanelPlugin<DebugPanelOptions>(DebugPanel).useFieldConfig().setPanelOptions((builder) => {
builder
.addRadio({
.addSelect({
path: 'mode',
name: 'Mode',
defaultValue: DebugMode.Render,
@@ -14,7 +14,9 @@ export const plugin = new PanelPlugin<DebugPanelOptions>(DebugPanel).useFieldCon
{ label: 'Render', value: DebugMode.Render },
{ label: 'Events', value: DebugMode.Events },
{ label: 'Cursor', value: DebugMode.Cursor },
{ label: 'Cursor', value: DebugMode.Cursor },
{ label: 'Share state', value: DebugMode.State },
{ label: 'Throw error', value: DebugMode.ThrowError },
],
},
})
+1
View File
@@ -13,6 +13,7 @@ export enum DebugMode {
Events = 'events',
Cursor = 'cursor',
State = 'State',
ThrowError = 'ThrowError',
}
export interface DebugPanelOptions {
@@ -0,0 +1,65 @@
import React, { FC, useCallback } from 'react';
import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import { ComparisonOperation, FeatureStyleConfig } from '../types';
import { Button } from '@grafana/ui';
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonMapper';
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
const { value, onChange, context } = props;
const OPTIONS = getComparisonOperatorOptions();
const onAddRule = useCallback(() => {
onChange([...value, DEFAULT_STYLE_RULE]);
}, [onChange, value]);
const onRuleChange = useCallback(
(idx) => (style: FeatureStyleConfig | undefined) => {
const copyStyles = [...value];
if (style) {
copyStyles[idx] = style;
} else {
//assume undefined is only returned on delete
copyStyles.splice(idx, 1);
}
onChange(copyStyles);
},
[onChange, value]
);
const styleOptions =
value &&
value.map((style, idx: number) => {
const itemSettings: StandardEditorsRegistryItem<any, StyleRuleEditorSettings> = {
settings: { options: OPTIONS },
} as any;
return (
<StyleRuleEditor
value={style}
onChange={onRuleChange(idx)}
context={context}
item={itemSettings}
key={`${idx}-${style.rule}`}
/>
);
});
return (
<>
{styleOptions}
<Button size="sm" icon="plus" onClick={onAddRule} variant="secondary" aria-label={'Add geomap style rule'}>
{'Add style rule'}
</Button>
</>
);
};
const getComparisonOperatorOptions = () => {
const options = [];
for (const value of Object.values(ComparisonOperation)) {
options.push({ value: value, label: value });
}
return options;
};
@@ -0,0 +1,163 @@
import React, { ChangeEvent, FC, useCallback } from 'react';
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
import { ComparisonOperation, FeatureStyleConfig } from '../types';
import { Button, ColorPicker, InlineField, InlineFieldRow, Input, Select, useStyles2 } from '@grafana/ui';
import { css } from '@emotion/css';
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
export interface StyleRuleEditorSettings {
options: SelectableValue[];
}
export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = (
props
) => {
const { value, onChange, item } = props;
const settings: StyleRuleEditorSettings = item.settings;
const styles = useStyles2(getStyles);
const LABEL_WIDTH = 10;
const onChangeComparisonProperty = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
rule: {
...value.rule,
property: e.currentTarget.value,
operation: value.rule?.operation ?? ComparisonOperation.EQ,
value: value.rule?.value ?? '',
},
});
},
[onChange, value]
);
const onChangeComparison = useCallback(
(selection: SelectableValue) => {
onChange({
...value,
rule: {
...value.rule,
operation: selection.value ?? ComparisonOperation.EQ,
property: value.rule?.property ?? '',
value: value.rule?.value ?? '',
},
});
},
[onChange, value]
);
const onChangeComparisonValue = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
rule: {
...value.rule,
value: e.currentTarget.value,
operation: value.rule?.operation ?? ComparisonOperation.EQ,
property: value.rule?.property ?? '',
},
});
},
[onChange, value]
);
const onChangeColor = useCallback(
(c: string) => {
onChange({ ...value, fillColor: c });
},
[onChange, value]
);
const onChangeStrokeWidth = useCallback(
(num: number | undefined) => {
onChange({ ...value, strokeWidth: num ?? value.strokeWidth ?? 1 });
},
[onChange, value]
);
const onDelete = useCallback(() => {
onChange(undefined);
}, [onChange]);
return (
<div className={styles.rule}>
<InlineFieldRow className={styles.row}>
<InlineField label="Rule" labelWidth={LABEL_WIDTH} grow={true}>
<Input
type="text"
placeholder={'Feature property'}
value={`${value?.rule?.property}`}
onChange={onChangeComparisonProperty}
aria-label={'Feature property'}
/>
</InlineField>
<InlineField className={styles.inline} grow={true}>
<Select
menuShouldPortal
value={`${value?.rule?.operation}` ?? ComparisonOperation.EQ}
options={settings.options}
onChange={onChangeComparison}
aria-label={'Comparison operator'}
/>
</InlineField>
<InlineField className={styles.inline} grow={true}>
<Input
type="text"
placeholder={'value'}
value={`${value?.rule?.value}`}
onChange={onChangeComparisonValue}
aria-label={'Comparison value'}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow className={styles.row}>
<InlineField label="Style" labelWidth={LABEL_WIDTH} className={styles.color}>
<ColorPicker color={value?.fillColor} onChange={onChangeColor} />
</InlineField>
<InlineField label="Stroke" className={styles.inline} grow={true}>
<NumberInput
value={value?.strokeWidth ?? 1}
min={1}
max={20}
step={0.5}
aria-label={'Stroke width'}
onChange={onChangeStrokeWidth}
/>
</InlineField>
<Button
size="md"
icon="trash-alt"
onClick={() => onDelete()}
variant="secondary"
aria-label={'Delete style rule'}
className={styles.button}
></Button>
</InlineFieldRow>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
rule: css`
margin-bottom: ${theme.spacing(1)};
`,
row: css`
display: flex;
margin-bottom: 4px;
`,
inline: css`
margin-bottom: 0;
margin-left: 4px;
`,
color: css`
align-items: center;
margin-bottom: 0;
margin-right: 4px;
`,
button: css`
margin-left: 4px;
`,
});
@@ -7,15 +7,16 @@ import { Feature } from 'ol';
import { Geometry } from 'ol/geom';
import { getGeoMapStyle } from '../../utils/getGeoMapStyle';
import { checkFeatureMatchesStyleRule } from '../../utils/checkFeatureMatchesStyleRule';
import { FeatureStyleConfig } from '../../types';
import { ComparisonOperation, FeatureStyleConfig } from '../../types';
import { Stroke, Style } from 'ol/style';
import { FeatureLike } from 'ol/Feature';
import { GeomapStyleRulesEditor } from '../../editor/GeomapStyleRulesEditor';
export interface GeoJSONMapperConfig {
// URL for a geojson file
src?: string;
// Styles that can be applied
styles?: FeatureStyleConfig[];
styles: FeatureStyleConfig[];
}
const defaultOptions: GeoJSONMapperConfig = {
@@ -23,6 +24,16 @@ const defaultOptions: GeoJSONMapperConfig = {
styles: [],
};
export const DEFAULT_STYLE_RULE: FeatureStyleConfig = {
fillColor: '#1F60C4',
strokeWidth: 1,
rule: {
property: '',
operation: ComparisonOperation.EQ,
value: '',
},
};
export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
id: 'geojson-value-mapper',
name: 'Map values to GeoJSON file',
@@ -44,8 +55,8 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
const defaultStyle = new Style({
stroke: new Stroke({
color: '#1F60C4',
width: 1,
color: DEFAULT_STYLE_RULE.fillColor,
width: DEFAULT_STYLE_RULE.strokeWidth,
}),
});
@@ -79,20 +90,27 @@ export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
// Geojson source url
registerOptionsUI: (builder) => {
builder.addSelect({
path: 'config.src',
name: 'GeoJSON URL',
settings: {
options: [
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
],
allowCustomValue: true,
},
defaultValue: defaultOptions.src,
});
builder
.addSelect({
path: 'config.src',
name: 'GeoJSON URL',
settings: {
options: [
{ label: 'public/maps/countries.geojson', value: 'public/maps/countries.geojson' },
{ label: 'public/maps/usa-states.geojson', value: 'public/maps/usa-states.geojson' },
],
allowCustomValue: true,
},
defaultValue: defaultOptions.src,
})
.addCustomEditor({
id: 'config.styles',
path: 'config.styles',
name: 'Style Rules',
editor: GeomapStyleRulesEditor,
settings: {},
defaultValue: [],
});
},
// fill in the default values
defaultOptions,
};
+19 -8
View File
@@ -53,14 +53,25 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
);
}
builder.addNestedOptions(
getLayerEditor({
category: ['Data layer'],
path: 'layers[0]', // only one for now
basemaps: false,
current: context.options?.layers?.[0],
})
);
let layerCount = context.options?.layers?.length;
if (layerCount == null || layerCount < 1) {
layerCount = 1;
}
for (let i = 0; i < layerCount; i++) {
let name = 'Data layer';
if (i > 0) {
name += ` (${i + 1})`;
}
builder.addNestedOptions(
getLayerEditor({
category: [name],
path: `layers[${i}]`, // only one for now
basemaps: false,
current: context.options?.layers?.[i],
})
);
}
// The controls section
category = ['Map controls'];
+6
View File
@@ -502,6 +502,12 @@ export function getAppRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "BenchmarksPage"*/ 'app/features/sandbox/BenchmarksPage')
),
},
{
path: '/sandbox/test',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "TestStuffPage"*/ 'app/features/sandbox/TestStuffPage')
),
},
{
path: '/dashboards/f/:uid/:slug/library-panels',
component: SafeDynamicImport(

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