diff --git a/.drone.yml b/.drone.yml index d026d45679c..b583de99033 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 04fcddc4b15..ad210108d6c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/Makefile b/Makefile index 8daf01d7f24..2a8fad66bd2 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index ec161a2353e..15b3f29cbd3 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -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. diff --git a/docs/sources/enterprise/enhanced_ldap.md b/docs/sources/enterprise/enhanced_ldap.md index 8ad81b76d68..f66ae04c00c 100644 --- a/docs/sources/enterprise/enhanced_ldap.md +++ b/docs/sources/enterprise/enhanced_ldap.md @@ -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. diff --git a/emails/assets/css/style.css b/emails/assets/css/style.css index 83f8d2c67a6..065421a173d 100644 --- a/emails/assets/css/style.css +++ b/emails/assets/css/style.css @@ -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) { diff --git a/emails/templates/ng_alert_notification.html b/emails/templates/ng_alert_notification.html index d68c13e9b02..ec700434b71 100644 --- a/emails/templates/ng_alert_notification.html +++ b/emails/templates/ng_alert_notification.html @@ -5,21 +5,21 @@ [[ range .Annotations.SortedPairs ]] -

[[ .Name ]]: [[ .Value ]]

+

[[ .Name ]]: [[ .Value ]]

[[ end ]] [[ end ]] - Labels: - + + Labels: - + [[ if .SilenceURL ]] - + -[[ end ]] \ No newline at end of file +[[ end ]] diff --git a/go.mod b/go.mod index 2bf2fdb90f6..f198c5df6a5 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 1d9f5ec86b5..3313e034611 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/latest.json b/latest.json index c32a916455f..32de21fec7a 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "8.2.1", - "testing": "8.2.1" + "stable": "8.2.2", + "testing": "8.2.2" } diff --git a/package.json b/package.json index 8e53525d566..ac64f90fa16 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index fff69c9c6e0..b66359958f9 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -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", diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 5ec011acb69..c69f4e4dbd9 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -53,6 +53,7 @@ export interface FeatureToggles { prometheusMonaco: boolean; newNavigation: boolean; fullRangeLogsVolume: boolean; + autoLoadFullRangeLogsVolume: boolean; } /** diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 55a0992670b..2240d60ba10 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -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", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index ac8b2e3ce1b..180dfad5559 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -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", diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index c2816f44d34..1249ab6fe25 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -69,6 +69,7 @@ export class GrafanaBootConfig implements GrafanaConfig { prometheusMonaco: false, newNavigation: false, fullRangeLogsVolume: false, + autoLoadFullRangeLogsVolume: false, }; licenseInfo: LicenseInfo = {} as LicenseInfo; rendererAvailable = false; diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index 4dc0cf4f823..c262da91434 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -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", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 2d3025fec34..25b1c2ed97d 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -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", diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx index a6f8368633f..ea648f1a84f 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx @@ -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( + + {({ error }) => { + if (!error) { + renderCount += 1; + return ; + } else { + return

{error.message}

; + } + }} +
+ ); + + await screen.findByText(problem.message); + + rerender( + + {({ error }) => { + if (!error) { + renderCount += 1; + return ; + } else { + return

{error.message}

; + } + }} +
+ ); + + expect(renderCount).toBe(2); + }); }); diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx index 3ec96221a1b..6496443c96f 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -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 { 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 . Default 'alertbox' */ style?: 'page' | 'alertbox'; + + /** Will re-render children after error if recover values changes */ + dependencies?: any[]; } export class ErrorBoundaryAlert extends PureComponent { @@ -69,10 +97,10 @@ export class ErrorBoundaryAlert extends PureComponent { }; render() { - const { title, children, style } = this.props; + const { title, children, style, dependencies } = this.props; return ( - + {({ error, errorInfo }) => { if (!errorInfo) { return children; diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 5fcdf98833e..218ebf9f989 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -88,6 +88,7 @@ const CustomControl = (props: any) => { export function SelectBase({ allowCustomValue = false, + allowCreateWhileLoading = false, 'aria-label': ariaLabel, autoFocus = false, backspaceRemovesValue = true, @@ -227,6 +228,7 @@ export function SelectBase({ if (allowCustomValue) { ReactSelectComponent = Creatable as any; + creatableProps.allowCreateWhileLoading = allowCreateWhileLoading; creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`); creatableProps.onCreateOption = onCreateOption; creatableProps.isValidNewOption = isValidNewOption; diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index 6da83a58e63..094d755f96a 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -9,6 +9,7 @@ export type InputActionMeta = { export interface SelectCommonProps { /** Aria label applied to the input field */ ['aria-label']?: string; + allowCreateWhileLoading?: boolean; allowCustomValue?: boolean; /** Focus is set to the Select when rendered*/ autoFocus?: boolean; diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx index d089d949227..90f7aa89935 100755 --- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx +++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx @@ -92,6 +92,15 @@ export class Sparkline extends PureComponent { 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), diff --git a/pkg/api/api.go b/pkg/api/api.go index e69a1f2dab3..359d2078fb6 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -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)) diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index cd41a815c0b..ff1dfce2ade 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -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 { diff --git a/pkg/api/pluginproxy/token_provider_gce.go b/pkg/api/pluginproxy/token_provider_gce.go index b1f7909c5da..2444b53634e 100644 --- a/pkg/api/pluginproxy/token_provider_gce.go +++ b/pkg/api/pluginproxy/token_provider_gce.go @@ -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) } diff --git a/pkg/api/pluginproxy/token_provider_jwt.go b/pkg/api/pluginproxy/token_provider_jwt.go index 27ee7e97af0..97a7ca0b55e 100644 --- a/pkg/api/pluginproxy/token_provider_jwt.go +++ b/pkg/api/pluginproxy/token_provider_jwt.go @@ -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) } diff --git a/pkg/api/pluginproxy/token_provider_test.go b/pkg/api/pluginproxy/token_provider_test.go index d2a89a89ace..baf3425bfe4 100644 --- a/pkg/api/pluginproxy/token_provider_test.go +++ b/pkg/api/pluginproxy/token_provider_test.go @@ -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) diff --git a/pkg/api/roles.go b/pkg/api/roles.go index fd7ad7c7261..c220948a2f0 100644 --- a/pkg/api/roles.go +++ b/pkg/api/roles.go @@ -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...) diff --git a/pkg/infra/kvstore/sql.go b/pkg/infra/kvstore/sql.go index a601608b244..829cabeaa83 100644 --- a/pkg/infra/kvstore/sql.go +++ b/pkg/infra/kvstore/sql.go @@ -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) } diff --git a/pkg/services/alerting/notifiers/alertmanager_test.go b/pkg/services/alerting/notifiers/alertmanager_test.go index e764d3e6fde..d9084879013 100644 --- a/pkg/services/alerting/notifiers/alertmanager_test.go +++ b/pkg/services/alerting/notifiers/alertmanager_test.go @@ -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"}) }) }) } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 24e08b39f23..32d68376b84 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -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) }) } diff --git a/pkg/services/alerting/notifiers/dingding_test.go b/pkg/services/alerting/notifiers/dingding_test.go index 6f193cfeda5..63143fb2631 100644 --- a/pkg/services/alerting/notifiers/dingding_test.go +++ b/pkg/services/alerting/notifiers/dingding_test.go @@ -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) }) }) } diff --git a/pkg/services/alerting/notifiers/discord_test.go b/pkg/services/alerting/notifiers/discord_test.go index a50209ea6ab..809d03df105 100644 --- a/pkg/services/alerting/notifiers/discord_test.go +++ b/pkg/services/alerting/notifiers/discord_test.go @@ -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) }) }) } diff --git a/pkg/services/alerting/notifiers/email_test.go b/pkg/services/alerting/notifiers/email_test.go index 6f01b020e65..2017e4da6c5 100644 --- a/pkg/services/alerting/notifiers/email_test.go +++ b/pkg/services/alerting/notifiers/email_test.go @@ -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]) }) }) } diff --git a/pkg/services/alerting/notifiers/googlechat_test.go b/pkg/services/alerting/notifiers/googlechat_test.go index 52b33a2c018..8cde1cd3547 100644 --- a/pkg/services/alerting/notifiers/googlechat_test.go +++ b/pkg/services/alerting/notifiers/googlechat_test.go @@ -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) }) }) } diff --git a/pkg/services/alerting/notifiers/hipchat_test.go b/pkg/services/alerting/notifiers/hipchat_test.go index cd3edc04cdc..b27e941d50f 100644 --- a/pkg/services/alerting/notifiers/hipchat_test.go +++ b/pkg/services/alerting/notifiers/hipchat_test.go @@ -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) }) }) } diff --git a/pkg/services/alerting/notifiers/kafka_test.go b/pkg/services/alerting/notifiers/kafka_test.go index 1b83e863748..86e0be76b28 100644 --- a/pkg/services/alerting/notifiers/kafka_test.go +++ b/pkg/services/alerting/notifiers/kafka_test.go @@ -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) }) }) } diff --git a/pkg/services/alerting/notifiers/line_test.go b/pkg/services/alerting/notifiers/line_test.go index 1fc3b0a59fc..be7559a10b2 100644 --- a/pkg/services/alerting/notifiers/line_test.go +++ b/pkg/services/alerting/notifiers/line_test.go @@ -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) }) } diff --git a/pkg/services/alerting/notifiers/opsgenie_test.go b/pkg/services/alerting/notifiers/opsgenie_test.go index d220eed8984..6e339071028 100644 --- a/pkg/services/alerting/notifiers/opsgenie_test.go +++ b/pkg/services/alerting/notifiers/opsgenie_test.go @@ -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": ""}) }) }) } diff --git a/pkg/services/alerting/notifiers/pagerduty_test.go b/pkg/services/alerting/notifiers/pagerduty_test.go index fac067a6717..fc6bcedc0af 100644 --- a/pkg/services/alerting/notifiers/pagerduty_test.go +++ b/pkg/services/alerting/notifiers/pagerduty_test.go @@ -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": "<>", - "custom_details": map[string]interface{}{ - "state": "alerting", - }, - "severity": "critical", - "summary": "someRule - someMessage", - "timestamp": "<>", - }, - "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": "<>", + "custom_details": map[string]interface{}{ + "state": "alerting", + }, + "severity": "critical", + "summary": "someRule - someMessage", + "timestamp": "<>", + }, + "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": "<>", - "custom_details": map[string]interface{}{ - "state": "alerting", - }, - "severity": "critical", - "summary": "someRule", - "timestamp": "<>", - }, - "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": "<>", + "custom_details": map[string]interface{}{ + "state": "alerting", + }, + "severity": "critical", + "summary": "someRule", + "timestamp": "<>", + }, + "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 <> - 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 <> + 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": "<>", + "custom_details": map[string]interface{}{ + "message": "someMessage", + "queries": map[string]interface{}{ + "someMetric": nil, }, - } + "state": "alerting", + }, + "severity": "critical", + "summary": "someRule", + "timestamp": "<>", + }, + "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": "<>", - "custom_details": map[string]interface{}{ - "message": "someMessage", - "queries": map[string]interface{}{ - "someMetric": nil, - }, - "state": "alerting", - }, - "severity": "critical", - "summary": "someRule", - "timestamp": "<>", - }, - "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": "<>", - "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": "<>", - "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": "<>", + "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": "<>", + "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": "<>", - "component": "aComponent", - "custom_details": map[string]interface{}{ - "group": "aGroup", - "class": "aClass", - "component": "aComponent", - "severity": "info", - "keyOnly": "", - "state": "alerting", - }, - "severity": "info", - "summary": "someRule - someMessage", - "timestamp": "<>", - "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": "<>", + "component": "aComponent", + "custom_details": map[string]interface{}{ + "group": "aGroup", + "class": "aClass", + "component": "aComponent", + "severity": "info", + "keyOnly": "", + "state": "alerting", + }, + "severity": "info", + "summary": "someRule - someMessage", + "timestamp": "<>", + "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": "<>", - "component": "aComponent", - "custom_details": map[string]interface{}{ - "group": "aGroup", - "class": "aClass", - "component": "aComponent", - "severity": "llama", - "keyOnly": "", - "state": "alerting", - }, - "severity": "critical", - "summary": "someRule - someMessage", - "timestamp": "<>", - "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": "<>", + "component": "aComponent", + "custom_details": map[string]interface{}{ + "group": "aGroup", + "class": "aClass", + "component": "aComponent", + "severity": "llama", + "keyOnly": "", + "state": "alerting", + }, + "severity": "critical", + "summary": "someRule - someMessage", + "timestamp": "<>", + "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) }) } diff --git a/pkg/services/alerting/notifiers/pushover_test.go b/pkg/services/alerting/notifiers/pushover_test.go index cef325a87c3..fe27b424b0f 100644 --- a/pkg/services/alerting/notifiers/pushover_test.go +++ b/pkg/services/alerting/notifiers/pushover_test.go @@ -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)) }) }) } diff --git a/pkg/services/alerting/notifiers/sensu_test.go b/pkg/services/alerting/notifiers/sensu_test.go index 11684e57e16..2be6f71d325 100644 --- a/pkg/services/alerting/notifiers/sensu_test.go +++ b/pkg/services/alerting/notifiers/sensu_test.go @@ -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) }) }) } diff --git a/pkg/services/alerting/notifiers/teams_test.go b/pkg/services/alerting/notifiers/teams_test.go index f1a267a66c2..3c43931ee8e 100644 --- a/pkg/services/alerting/notifiers/teams_test.go +++ b/pkg/services/alerting/notifiers/teams_test.go @@ -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) }) }) } diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go index 7d2424b1324..b0b1af92de8 100644 --- a/pkg/services/alerting/notifiers/telegram_test.go +++ b/pkg/services/alerting/notifiers/telegram_test.go @@ -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") }) }) }) diff --git a/pkg/services/alerting/notifiers/threema_test.go b/pkg/services/alerting/notifiers/threema_test.go index 6ccfcee7f75..4800afbe53e 100644 --- a/pkg/services/alerting/notifiers/threema_test.go +++ b/pkg/services/alerting/notifiers/threema_test.go @@ -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) }) }) } diff --git a/pkg/services/alerting/notifiers/victorops_test.go b/pkg/services/alerting/notifiers/victorops_test.go index 264a85b0907..8678e0da1ac 100644 --- a/pkg/services/alerting/notifiers/victorops_test.go +++ b/pkg/services/alerting/notifiers/victorops_test.go @@ -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) }) }) } diff --git a/pkg/services/libraryelements/libraryelements_patch_test.go b/pkg/services/libraryelements/libraryelements_patch_test.go index a2b9f414249..1921c1fb33a 100644 --- a/pkg/services/libraryelements/libraryelements_patch_test.go +++ b/pkg/services/libraryelements/libraryelements_patch_test.go @@ -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) diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index 6e88ad866a6..0196aa1507f 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -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)) diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index bc89824e03e..af6dc8d00e6 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -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 diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 699af43a8cc..5c1f9159ecc 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -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", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 263e3cce322..7914844df1d 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -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": { diff --git a/pkg/services/ngalert/notifier/channels/alertmanager.go b/pkg/services/ngalert/notifier/channels/alertmanager.go index 8f62c3969ee..99485420e14 100644 --- a/pkg/services/ngalert/notifier/channels/alertmanager.go +++ b/pkg/services/ngalert/notifier/channels/alertmanager.go @@ -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 diff --git a/pkg/services/ngalert/notifier/channels/base.go b/pkg/services/ngalert/notifier/channels/base.go new file mode 100644 index 00000000000..6f261bc7ef0 --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/base.go @@ -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), + } +} diff --git a/pkg/services/ngalert/notifier/channels/dingding.go b/pkg/services/ngalert/notifier/channels/dingding.go index 9f06f0ec56c..e6ac8d5f895 100644 --- a/pkg/services/ngalert/notifier/channels/dingding.go +++ b/pkg/services/ngalert/notifier/channels/dingding.go @@ -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 diff --git a/pkg/services/ngalert/notifier/channels/discord.go b/pkg/services/ngalert/notifier/channels/discord.go index 0c086a96f71..faa1b53093a 100644 --- a/pkg/services/ngalert/notifier/channels/discord.go +++ b/pkg/services/ngalert/notifier/channels/discord.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index 5deb62ea829..574f3d2ff91 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/googlechat.go b/pkg/services/ngalert/notifier/channels/googlechat.go index 2f0448ac723..307026fb8cf 100644 --- a/pkg/services/ngalert/notifier/channels/googlechat.go +++ b/pkg/services/ngalert/notifier/channels/googlechat.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/kafka.go b/pkg/services/ngalert/notifier/channels/kafka.go index d9bad9e777d..bcfd9d052c0 100644 --- a/pkg/services/ngalert/notifier/channels/kafka.go +++ b/pkg/services/ngalert/notifier/channels/kafka.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/line.go b/pkg/services/ngalert/notifier/channels/line.go index 6b631770c66..9cebd032c60 100644 --- a/pkg/services/ngalert/notifier/channels/line.go +++ b/pkg/services/ngalert/notifier/channels/line.go @@ -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 diff --git a/pkg/services/ngalert/notifier/channels/opsgenie.go b/pkg/services/ngalert/notifier/channels/opsgenie.go index 2713c82d929..b33b806a19c 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go index a46cb27c4aa..1b3fb980a72 100644 --- a/pkg/services/ngalert/notifier/channels/pagerduty.go +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/pushover.go b/pkg/services/ngalert/notifier/channels/pushover.go index 9f3ce6351c1..ad7ff9bc830 100644 --- a/pkg/services/ngalert/notifier/channels/pushover.go +++ b/pkg/services/ngalert/notifier/channels/pushover.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/sensugo.go b/pkg/services/ngalert/notifier/channels/sensugo.go index f5fb7427879..d93e2bcb8ef 100644 --- a/pkg/services/ngalert/notifier/channels/sensugo.go +++ b/pkg/services/ngalert/notifier/channels/sensugo.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index 65e53049f9d..6cd9fa28c1b 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/teams.go b/pkg/services/ngalert/notifier/channels/teams.go index 09599576746..2dfa4927851 100644 --- a/pkg/services/ngalert/notifier/channels/teams.go +++ b/pkg/services/ngalert/notifier/channels/teams.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/telegram.go b/pkg/services/ngalert/notifier/channels/telegram.go index cd2e93356b4..cea09366e0f 100644 --- a/pkg/services/ngalert/notifier/channels/telegram.go +++ b/pkg/services/ngalert/notifier/channels/telegram.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/threema.go b/pkg/services/ngalert/notifier/channels/threema.go index 80c02d830ff..d1ed3244511 100644 --- a/pkg/services/ngalert/notifier/channels/threema.go +++ b/pkg/services/ngalert/notifier/channels/threema.go @@ -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, diff --git a/pkg/services/ngalert/notifier/channels/victorops.go b/pkg/services/ngalert/notifier/channels/victorops.go index 4a1c74342fb..39d9dfaf9ad 100644 --- a/pkg/services/ngalert/notifier/channels/victorops.go +++ b/pkg/services/ngalert/notifier/channels/victorops.go @@ -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 diff --git a/pkg/services/ngalert/notifier/channels/webhook.go b/pkg/services/ngalert/notifier/channels/webhook.go index e8278145d75..c2d2ea3f8e8 100644 --- a/pkg/services/ngalert/notifier/channels/webhook.go +++ b/pkg/services/ngalert/notifier/channels/webhook.go @@ -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, diff --git a/pkg/services/ngalert/notifier/receivers.go b/pkg/services/ngalert/notifier/receivers.go index 4985183d102..6fbcd9f6875 100644 --- a/pkg/services/ngalert/notifier/receivers.go +++ b/pkg/services/ngalert/notifier/receivers.go @@ -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 { diff --git a/pkg/services/sqlstore/migrations/ualert/channel.go b/pkg/services/sqlstore/migrations/ualert/channel.go index d55d4e63e2c..81c078c550e 100644 --- a/pkg/services/sqlstore/migrations/ualert/channel.go +++ b/pkg/services/sqlstore/migrations/ualert/channel.go @@ -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, diff --git a/pkg/services/sqlstore/migrations/ualert/channel_test.go b/pkg/services/sqlstore/migrations/ualert/channel_test.go deleted file mode 100644 index 26ea14e6a95..00000000000 --- a/pkg/services/sqlstore/migrations/ualert/channel_test.go +++ /dev/null @@ -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 ¬ificationChannel{ - 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�{�" diff --git a/pkg/services/sqlstore/migrations/ualert/testing.go b/pkg/services/sqlstore/migrations/ualert/testing.go new file mode 100644 index 00000000000..af99d06e665 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/testing.go @@ -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{}), + } +} diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index 6ca453d356a..9ead2184493 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -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"` diff --git a/pkg/services/sqlstore/migrations/ualert/ualert_test.go b/pkg/services/sqlstore/migrations/ualert/ualert_test.go new file mode 100644 index 00000000000..3b9dd0ea165 --- /dev/null +++ b/pkg/services/sqlstore/migrations/ualert/ualert_test.go @@ -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�{�" diff --git a/pkg/tests/api/alerting/api_notification_channel_test.go b/pkg/tests/api/alerting/api_notification_channel_test.go index db844620d63..41e9dc90aa5 100644 --- a/pkg/tests/api/alerting/api_notification_channel_test.go +++ b/pkg/tests/api/alerting/api_notification_channel_test.go @@ -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) }) } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 32f4fc890d2..6043861773e 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -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", diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index 0d91b89b1d2..e3625618c2a 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -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{ diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 3780b8da72c..e0761c722ff 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -68,6 +68,7 @@ export const TagFilter: FC = ({ const value = tags.map((tag) => ({ value: tag, label: tag, count: 0 })); const selectOptions = { + allowCreateWhileLoading: true, allowCustomValue, formatCreateLabel, defaultOptions: true, diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx index 14f4dc1602e..387ac9e0961 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategory.tsx @@ -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 = 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(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 = React.memo( className={boxStyles} data-testid="options-category" aria-label={selectors.components.OptionsGroup.group(id)} + ref={ref} >
diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx index 67a5e4a2190..6287fcc8e64 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneOptions.test.tsx @@ -21,6 +21,12 @@ standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs); const mockStore = configureMockStore(); 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(); diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 44f89fa57e6..7f9995eef85 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -309,12 +309,17 @@ export class PanelChrome extends PureComponent { 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 { } 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 { alertState={alertState} data={data} /> - + {({ error }) => { if (error) { - this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR); return null; } return this.renderPanel(width, height); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index d9e44cbaa47..9deb905f8a0 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -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 { getFieldLinks={this.getFieldLinks} addResultsToCache={() => addResultsToCache(exploreId)} clearCache={() => clearCache(exploreId)} - loadingLogsVolumeAvailable={hasLogsVolumeSupport(datasourceInstance) && !!logsVolumeDataProvider} + loadingLogsVolumeAvailable={ + hasLogsVolumeSupport(datasourceInstance) && + !!logsVolumeDataProvider && + !config.featureToggles.autoLoadFullRangeLogsVolume + } onClickLoadLogsVolume={() => loadLogsVolumeData(exploreId)} /> diff --git a/public/app/features/explore/SecondaryActions.test.tsx b/public/app/features/explore/SecondaryActions.test.tsx index 6b55113f6d8..1b49db65815 100644 --- a/public/app/features/explore/SecondaryActions.test.tsx +++ b/public/app/features/explore/SecondaryActions.test.tsx @@ -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; + }); }); diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 8d501fffc1c..c578e599dcb 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -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; 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; }; + }); + 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); + }); }); }); diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index 306d3d446cf..d6028ece374 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -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( diff --git a/public/app/features/panel/components/PanelRenderer.tsx b/public/app/features/panel/components/PanelRenderer.tsx index c1358f94139..72ceb49bafd 100644 --- a/public/app/features/panel/components/PanelRenderer.tsx +++ b/public/app/features/panel/components/PanelRenderer.tsx @@ -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

(pr const PanelComponent = plugin.panel; return ( - str} - onOptionsChange={onOptionsChange} - onFieldConfigChange={setFieldConfig} - onChangeTimeRange={onChangeTimeRange} - eventBus={appEvents} - /> + + str} + onOptionsChange={onOptionsChange} + onFieldConfigChange={setFieldConfig} + onChangeTimeRange={onChangeTimeRange} + eventBus={appEvents} + /> + ); } diff --git a/public/app/features/sandbox/TestStuffPage.tsx b/public/app/features/sandbox/TestStuffPage.tsx index 6f17f924e0f..5430cc72b75 100644 --- a/public/app/features/sandbox/TestStuffPage.tsx +++ b/public/app/features/sandbox/TestStuffPage.tsx @@ -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 (

- diff --git a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx index c91b0fb3436..acd086f25fe 100644 --- a/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx +++ b/public/app/plugins/datasource/prometheus/components/PrometheusMetricsBrowser.tsx @@ -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 { 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 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) { diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 9e71fe63408..3854ebf0a34 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -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])))'; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index da9dba15f33..8cd551e8b7f 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -894,6 +894,7 @@ export class PrometheusDatasource extends DataSourceWithBackend { render() { const { options } = this.props; - if (options.mode === DebugMode.Events) { - return ; + switch (options.mode) { + case DebugMode.Events: + return ; + case DebugMode.Cursor: + return ; + case DebugMode.State: + return ; + case DebugMode.ThrowError: + throw new Error('I failed you and for that i am deeply sorry'); + default: + return ; } - - if (options.mode === DebugMode.Cursor) { - return ; - } - - if (options.mode === DebugMode.State) { - return ; - } - - return ; } } diff --git a/public/app/plugins/panel/debug/module.tsx b/public/app/plugins/panel/debug/module.tsx index a5169af95fe..a2bfecebece 100644 --- a/public/app/plugins/panel/debug/module.tsx +++ b/public/app/plugins/panel/debug/module.tsx @@ -5,7 +5,7 @@ import { DebugMode, DebugPanelOptions } from './types'; export const plugin = new PanelPlugin(DebugPanel).useFieldConfig().setPanelOptions((builder) => { builder - .addRadio({ + .addSelect({ path: 'mode', name: 'Mode', defaultValue: DebugMode.Render, @@ -14,7 +14,9 @@ export const plugin = new PanelPlugin(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 }, ], }, }) diff --git a/public/app/plugins/panel/debug/types.ts b/public/app/plugins/panel/debug/types.ts index 0db4d56f237..d4563b13826 100644 --- a/public/app/plugins/panel/debug/types.ts +++ b/public/app/plugins/panel/debug/types.ts @@ -13,6 +13,7 @@ export enum DebugMode { Events = 'events', Cursor = 'cursor', State = 'State', + ThrowError = 'ThrowError', } export interface DebugPanelOptions { diff --git a/public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx b/public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx new file mode 100644 index 00000000000..4b2c8ffe6af --- /dev/null +++ b/public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx @@ -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> = (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 = { + settings: { options: OPTIONS }, + } as any; + + return ( + + ); + }); + + return ( + <> + {styleOptions} + + + ); +}; + +const getComparisonOperatorOptions = () => { + const options = []; + for (const value of Object.values(ComparisonOperation)) { + options.push({ value: value, label: value }); + } + return options; +}; diff --git a/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx b/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx new file mode 100644 index 00000000000..87b1f315dd7 --- /dev/null +++ b/public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx @@ -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> = ( + props +) => { + const { value, onChange, item } = props; + const settings: StyleRuleEditorSettings = item.settings; + + const styles = useStyles2(getStyles); + + const LABEL_WIDTH = 10; + + const onChangeComparisonProperty = useCallback( + (e: ChangeEvent) => { + 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) => { + 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 ( +
+ + + + + + + + + + + + + + + + + +
+ ); +}; + +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; + `, +}); diff --git a/public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts b/public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts index 309086d590c..f5507acf912 100644 --- a/public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts +++ b/public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts @@ -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 = { id: 'geojson-value-mapper', name: 'Map values to GeoJSON file', @@ -44,8 +55,8 @@ export const geojsonMapper: MapLayerRegistryItem = { 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 = { // 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, }; diff --git a/public/app/plugins/panel/geomap/module.tsx b/public/app/plugins/panel/geomap/module.tsx index 73d7507086d..90ee0aefd55 100644 --- a/public/app/plugins/panel/geomap/module.tsx +++ b/public/app/plugins/panel/geomap/module.tsx @@ -53,14 +53,25 @@ export const plugin = new PanelPlugin(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']; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index bc85a765316..5cfde164306 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -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( diff --git a/public/emails/alert_notification.html b/public/emails/alert_notification.html index 1ffbd182400..8d7917c92d4 100644 --- a/public/emails/alert_notification.html +++ b/public/emails/alert_notification.html @@ -344,7 +344,7 @@ text-decoration: underline;
- + - +