mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into master
This commit is contained in:
commit
de22e793d8
@ -7,6 +7,8 @@ indent_size = 2
|
|||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
max_line_length = 120
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.go]
|
[*.go]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -39,6 +39,7 @@ conf/custom.ini
|
|||||||
fig.yml
|
fig.yml
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
|
/conf/provisioning/**/custom.yaml
|
||||||
profile.cov
|
profile.cov
|
||||||
/grafana
|
/grafana
|
||||||
.notouch
|
.notouch
|
||||||
@ -50,6 +51,9 @@ debug.test
|
|||||||
/packaging/**/*.rpm
|
/packaging/**/*.rpm
|
||||||
/packaging/**/*.deb
|
/packaging/**/*.deb
|
||||||
|
|
||||||
|
# Ignore OSX indexing
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
/vendor/**/*.py
|
/vendor/**/*.py
|
||||||
/vendor/**/*.xml
|
/vendor/**/*.xml
|
||||||
/vendor/**/*.yml
|
/vendor/**/*.yml
|
||||||
|
49
CHANGELOG.md
49
CHANGELOG.md
@ -1,13 +1,31 @@
|
|||||||
# 5.0.0 (unreleased)
|
# 5.0.0 (unreleased / master branch)
|
||||||
|
|
||||||
### WIP (in develop branch currently as its unstable or unfinished)
|
Grafana v5.0 is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5.
|
||||||
- Dashboard folders
|
|
||||||
- User groups
|
|
||||||
- Dashboard permissions (on folder & dashboard level), permissions can be assigned to groups or individual users
|
|
||||||
- UX changes to nav & side menu
|
|
||||||
- New dashboard grid layout system
|
|
||||||
|
|
||||||
# 4.7.0 (unreleased)
|
### New Features
|
||||||
|
- **Dashboards** Dashboard folders, [#1611](https://github.com/grafana/grafana/issues/1611)
|
||||||
|
- **Teams** User groups (teams) implemented. Can be used in folder & dashboard permission list.
|
||||||
|
- **Dashboard grid**: Panels are now layed out in a two dimensional grid (with x, y, w, h). [#9093](https://github.com/grafana/grafana/issues/9093).
|
||||||
|
- **Templating**: Vertical repeat direction for panel repeats.
|
||||||
|
- **UX**: Major update to page header and navigation
|
||||||
|
- **Dashboard settings**: Combine dashboard settings views into one with side menu, [#9750](https://github.com/grafana/grafana/issues/9750)
|
||||||
|
|
||||||
|
## New Dashboard Grid
|
||||||
|
|
||||||
|
The new grid engine is major upgrade for how you can position and move panels. It enables new layouts and a much easier dashboard building experience. The change is backwards compatible. Grafana will automatically upgrade your dashboards to the new schema and position panels to match your existing layout. There might be minor differences in panel height.
|
||||||
|
|
||||||
|
Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: 24, h: 5}`. Units are in grid dimensions (24 columns, 1 height unit 30px). Rows and Panels objects exist (together) in a flat array directly on the dashboard root object. Rows are not needed for layouts anymore and are mainly there for backward compatibility. Some panel plugins that do not respect their panel height might require an update.
|
||||||
|
|
||||||
|
# 4.7.0 (unreleased / v4.7.x branch)
|
||||||
|
|
||||||
|
## Breaking changes
|
||||||
|
|
||||||
|
`[dashboard.json]` have been replaced with [dashboard provisioning](http://docs.grafana.org/administration/provisioning/).
|
||||||
|
|
||||||
|
Config files for provisioning datasources as configuration have changed from `/conf/datasources` to `/conf/provisioning/datasources`.
|
||||||
|
From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when installed with deb/rpm packages.
|
||||||
|
|
||||||
|
The pagerduty notifier now defaults to not auto resolve incidents. More details at [#10222](https://github.com/grafana/grafana/issues/10222)
|
||||||
|
|
||||||
## New Features
|
## New Features
|
||||||
* **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson)
|
* **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson)
|
||||||
@ -17,27 +35,38 @@
|
|||||||
* **Datasources**: Its now possible to configure datasources with config files [#1789](https://github.com/grafana/grafana/issues/1789)
|
* **Datasources**: Its now possible to configure datasources with config files [#1789](https://github.com/grafana/grafana/issues/1789)
|
||||||
* **Graphite**: Query editor updated to support new query by tag features [#9230](https://github.com/grafana/grafana/issues/9230)
|
* **Graphite**: Query editor updated to support new query by tag features [#9230](https://github.com/grafana/grafana/issues/9230)
|
||||||
* **Dashboard history**: New config file option versions_to_keep sets how many versions per dashboard to store, [#9671](https://github.com/grafana/grafana/issues/9671)
|
* **Dashboard history**: New config file option versions_to_keep sets how many versions per dashboard to store, [#9671](https://github.com/grafana/grafana/issues/9671)
|
||||||
|
* **Dashboard as cfg**: Load dashboards from file into Grafana on startup/change [#9654](https://github.com/grafana/grafana/issues/9654) [#5269](https://github.com/grafana/grafana/issues/5269)
|
||||||
|
* **Prometheus**: Grafana can now send alerts to Prometheus Alertmanager while firing [#7481](https://github.com/grafana/grafana/issues/7481), thx [@Thib17](https://github.com/Thib17) and [@mtanda](https://github.com/mtanda)
|
||||||
|
* **Table**: Support multiple table formated queries in table panel [#9170](https://github.com/grafana/grafana/issues/9170), thx [@davkal](https://github.com/davkal)
|
||||||
## Minor
|
## Minor
|
||||||
* **Alert panel**: Adds placeholder text when no alerts are within the time range [#9624](https://github.com/grafana/grafana/issues/9624), thx [@straend](https://github.com/straend)
|
* **Alert panel**: Adds placeholder text when no alerts are within the time range [#9624](https://github.com/grafana/grafana/issues/9624), thx [@straend](https://github.com/straend)
|
||||||
* **Mysql**: MySQL enable MaxOpenCon and MaxIdleCon regards how constring is configured. [#9784](https://github.com/grafana/grafana/issues/9784), thx [@dfredell](https://github.com/dfredell)
|
* **Mysql**: MySQL enable MaxOpenCon and MaxIdleCon regards how constring is configured. [#9784](https://github.com/grafana/grafana/issues/9784), thx [@dfredell](https://github.com/dfredell)
|
||||||
* **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda)
|
* **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Dashboard**: Make it possible to start dashboards from search and dashboard list panel [#1871](https://github.com/grafana/grafana/issues/1871)
|
* **Dashboard**: Make it possible to start dashboards from search and dashboard list panel [#1871](https://github.com/grafana/grafana/issues/1871)
|
||||||
* **Annotations**: Posting annotations now return the id of the annotation [#9798](https://github.com/grafana/grafana/issues/9798)
|
* **Annotations**: Posting annotations now return the id of the annotation [#9798](https://github.com/grafana/grafana/issues/9798)
|
||||||
|
* **Systemd**: Use systemd notification ready flag [#10024](https://github.com/grafana/grafana/issues/10024), thx [@jgrassler](https://github.com/jgrassler)
|
||||||
|
* **Github**: Use organizations_url provided from github to verify user belongs in org. [#10111](https://github.com/grafana/grafana/issues/10111), thx
|
||||||
|
[@adiletmaratov](https://github.com/adiletmaratov)
|
||||||
|
* **Backend**: Fixed bug where Grafana exited before all sub routines where finished [#10131](https://github.com/grafana/grafana/issues/10131)
|
||||||
|
* **Azure**: Adds support for Azure blob storage as external image stor [#8955](https://github.com/grafana/grafana/issues/8955), thx [@saada](https://github.com/saada)
|
||||||
|
|
||||||
## Tech
|
## Tech
|
||||||
* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
|
* **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645)
|
||||||
|
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
* **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand)
|
* **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand)
|
||||||
* **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
* **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu)
|
||||||
* **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
|
* **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm)
|
||||||
|
* **Pagerduty**: Pagerduty dont auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222)
|
||||||
|
|
||||||
# 4.6.3 (unreleased)
|
# 4.6.3 (2017-12-14)
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
* **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952)
|
* **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952)
|
||||||
* **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951)
|
* **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951)
|
||||||
|
* **Alerting**: Fixes bug where rules evaluated as firing when all conditions was false and using OR operator. [#9318](https://github.com/grafana/grafana/issues/9318)
|
||||||
|
* **Cloudwatch**: CloudWatch no longer display metrics' default alias [#10151](https://github.com/grafana/grafana/issues/10151), thx [@mtanda](https://github.com/mtanda)
|
||||||
|
|
||||||
# 4.6.2 (2017-11-16)
|
# 4.6.2 (2017-11-16)
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Grafana v5 Alpha Preview
|
||||||
|
Grafana master is now v5.0 alpha. This is going to be the biggest and most foundational release Grafana has ever had, coming with a ton of UX improvements, a new dashboard grid engine, dashboard folders, user teams and permissions. Checkout out this [video preview](https://www.youtube.com/watch?v=BC_YRNpqj5k) of Grafana v5.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get)
|
Head to [docs.grafana.org](http://docs.grafana.org/installation/) and [download](https://grafana.com/get)
|
||||||
the latest release.
|
the latest release.
|
||||||
@ -19,7 +22,7 @@ If you have any problems please read the [troubleshooting guide](http://docs.gra
|
|||||||
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
|
Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
|
||||||
|
|
||||||
## Run from master
|
## Run from master
|
||||||
If you want to build a package yourself, or contribute. Here is a guide for how to do that. You can always find
|
If you want to build a package yourself, or contribute - Here is a guide for how to do that. You can always find
|
||||||
the latest master builds [here](https://grafana.com/grafana/download)
|
the latest master builds [here](https://grafana.com/grafana/download)
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
@ -97,7 +100,7 @@ Writing & watching frontend tests (we have two test runners)
|
|||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
If you have any idea for an improvement or found a bug do not hesitate to open an issue.
|
If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
|
||||||
And if you have time clone this repo and submit a pull request and help me make Grafana
|
And if you have time clone this repo and submit a pull request and help me make Grafana
|
||||||
the kickass metrics & devops dashboard we all dream about!
|
the kickass metrics & devops dashboard we all dream about!
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ But it will give you an idea of our current vision and plan.
|
|||||||
### Short term (1-4 months)
|
### Short term (1-4 months)
|
||||||
|
|
||||||
- Release Grafana v5
|
- Release Grafana v5
|
||||||
- User groups
|
- Teams
|
||||||
- Dashboard folders
|
- Dashboard folders
|
||||||
- Dashboard & folder permissions (assigned to users or groups)
|
- Dashboard & folder permissions (assigned to users or groups)
|
||||||
- New Dashboard layout engine
|
- New Dashboard layout engine
|
||||||
|
@ -20,8 +20,8 @@ logs = data/log
|
|||||||
# Directory where grafana will automatically scan and look for plugins
|
# Directory where grafana will automatically scan and look for plugins
|
||||||
plugins = data/plugins
|
plugins = data/plugins
|
||||||
|
|
||||||
# Config files containing datasources that will be configured at startup
|
# folder that contains provisioning config files that grafana will apply on startup and while running.
|
||||||
datasources = conf/datasources
|
provisioning = conf/provisioning
|
||||||
|
|
||||||
#################################### Server ##############################
|
#################################### Server ##############################
|
||||||
[server]
|
[server]
|
||||||
@ -221,6 +221,9 @@ external_manage_link_url =
|
|||||||
external_manage_link_name =
|
external_manage_link_name =
|
||||||
external_manage_info =
|
external_manage_info =
|
||||||
|
|
||||||
|
# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
|
||||||
|
viewers_can_edit = false
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
# Set to true to disable (hide) the login form, useful if you use OAuth
|
# Set to true to disable (hide) the login form, useful if you use OAuth
|
||||||
disable_login_form = false
|
disable_login_form = false
|
||||||
@ -391,11 +394,6 @@ facility =
|
|||||||
# Syslog tag. By default, the process' argv[0] is used.
|
# Syslog tag. By default, the process' argv[0] is used.
|
||||||
tag =
|
tag =
|
||||||
|
|
||||||
#################################### Dashboard JSON files ################
|
|
||||||
[dashboards.json]
|
|
||||||
enabled = false
|
|
||||||
path = /var/lib/grafana/dashboards
|
|
||||||
|
|
||||||
#################################### Usage Quotas ########################
|
#################################### Usage Quotas ########################
|
||||||
[quota]
|
[quota]
|
||||||
enabled = false
|
enabled = false
|
||||||
@ -475,7 +473,7 @@ sampler_param = 1
|
|||||||
|
|
||||||
#################################### External Image Storage ##############
|
#################################### External Image Storage ##############
|
||||||
[external_image_storage]
|
[external_image_storage]
|
||||||
# You can choose between (s3, webdav, gcs)
|
# You can choose between (s3, webdav, gcs, azure_blob)
|
||||||
provider =
|
provider =
|
||||||
|
|
||||||
[external_image_storage.s3]
|
[external_image_storage.s3]
|
||||||
@ -496,3 +494,8 @@ public_url =
|
|||||||
key_file =
|
key_file =
|
||||||
bucket =
|
bucket =
|
||||||
path =
|
path =
|
||||||
|
|
||||||
|
[external_image_storage.azure_blob]
|
||||||
|
account_name =
|
||||||
|
account_key =
|
||||||
|
container_name =
|
||||||
|
6
conf/provisioning/dashboards/sample.yaml
Normal file
6
conf/provisioning/dashboards/sample.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# - name: 'default'
|
||||||
|
# org_id: 1
|
||||||
|
# folder: ''
|
||||||
|
# type: file
|
||||||
|
# options:
|
||||||
|
# folder: /var/lib/grafana/dashboards
|
@ -1,11 +1,11 @@
|
|||||||
# list of datasources that should be deleted from the database
|
# # list of datasources that should be deleted from the database
|
||||||
delete_datasources:
|
#delete_datasources:
|
||||||
# - name: Graphite
|
# - name: Graphite
|
||||||
# org_id: 1
|
# org_id: 1
|
||||||
|
|
||||||
# list of datasources to insert/update depending
|
# # list of datasources to insert/update depending
|
||||||
# whats available in the datbase
|
# # whats available in the datbase
|
||||||
datasources:
|
#datasources:
|
||||||
# # <string, required> name of the datasource. Required
|
# # <string, required> name of the datasource. Required
|
||||||
# - name: Graphite
|
# - name: Graphite
|
||||||
# # <string, required> datasource type. Required
|
# # <string, required> datasource type. Required
|
@ -20,8 +20,8 @@
|
|||||||
# Directory where grafana will automatically scan and look for plugins
|
# Directory where grafana will automatically scan and look for plugins
|
||||||
;plugins = /var/lib/grafana/plugins
|
;plugins = /var/lib/grafana/plugins
|
||||||
|
|
||||||
# Config files containing datasources that will be configured at startup
|
# folder that contains provisioning config files that grafana will apply on startup and while running.
|
||||||
;datasources = conf/datasources
|
; provisioning = conf/provisioning
|
||||||
|
|
||||||
#################################### Server ####################################
|
#################################### Server ####################################
|
||||||
[server]
|
[server]
|
||||||
@ -205,6 +205,9 @@ log_queries =
|
|||||||
;external_manage_link_name =
|
;external_manage_link_name =
|
||||||
;external_manage_info =
|
;external_manage_info =
|
||||||
|
|
||||||
|
# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
|
||||||
|
;viewers_can_edit = false
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
|
# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
|
||||||
;disable_login_form = false
|
;disable_login_form = false
|
||||||
@ -367,11 +370,6 @@ log_queries =
|
|||||||
;tag =
|
;tag =
|
||||||
|
|
||||||
|
|
||||||
;#################################### Dashboard JSON files ##########################
|
|
||||||
[dashboards.json]
|
|
||||||
;enabled = false
|
|
||||||
;path = /var/lib/grafana/dashboards
|
|
||||||
|
|
||||||
#################################### Alerting ############################
|
#################################### Alerting ############################
|
||||||
[alerting]
|
[alerting]
|
||||||
# Disable alerting engine & UI features
|
# Disable alerting engine & UI features
|
||||||
@ -419,7 +417,7 @@ log_queries =
|
|||||||
#################################### External image storage ##########################
|
#################################### External image storage ##########################
|
||||||
[external_image_storage]
|
[external_image_storage]
|
||||||
# Used for uploading images to public servers so they can be included in slack/email messages.
|
# Used for uploading images to public servers so they can be included in slack/email messages.
|
||||||
# you can choose between (s3, webdav, gcs)
|
# you can choose between (s3, webdav, gcs, azure_blob)
|
||||||
;provider =
|
;provider =
|
||||||
|
|
||||||
[external_image_storage.s3]
|
[external_image_storage.s3]
|
||||||
@ -439,3 +437,8 @@ log_queries =
|
|||||||
;key_file =
|
;key_file =
|
||||||
;bucket =
|
;bucket =
|
||||||
;path =
|
;path =
|
||||||
|
|
||||||
|
[external_image_storage.azure_blob]
|
||||||
|
;account_name =
|
||||||
|
;account_key =
|
||||||
|
;container_name =
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
graphite:
|
graphite09:
|
||||||
build: blocks/graphite
|
build: blocks/graphite
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
|
@ -7,3 +7,4 @@
|
|||||||
MYSQL_PASSWORD: password
|
MYSQL_PASSWORD: password
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
|
tmpfs: /var/lib/mysql:rw
|
||||||
|
@ -5,3 +5,4 @@
|
|||||||
POSTGRES_PASSWORD: grafanatest
|
POSTGRES_PASSWORD: grafanatest
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
tmpfs: /var/lib/postgresql/data:rw
|
@ -65,15 +65,16 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe
|
|||||||
Tool | Project
|
Tool | Project
|
||||||
-----|------------
|
-----|------------
|
||||||
Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana)
|
Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana)
|
||||||
|
Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana)
|
||||||
Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana)
|
Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana)
|
||||||
Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
|
Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
|
||||||
Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
|
Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
|
||||||
|
|
||||||
## Datasources
|
## Datasources
|
||||||
|
|
||||||
> This feature is available from v4.7
|
> This feature is available from v5.0
|
||||||
|
|
||||||
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`conf/datasources`](/installation/configuration/#datasources) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
|
It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
|
||||||
|
|
||||||
### Running multiple grafana instances.
|
### Running multiple grafana instances.
|
||||||
If you are running multiple instances of Grafana you might run into problems if they have different versions of the datasource.yaml configuration file. The best way to solve this problem is to add a version number to each datasource in the configuration and increase it when you update the config. Grafana will only update datasources with the same or lower version number than specified in the config. That way old configs cannot overwrite newer configs if they restart at the same time.
|
If you are running multiple instances of Grafana you might run into problems if they have different versions of the datasource.yaml configuration file. The best way to solve this problem is to add a version number to each datasource in the configuration and increase it when you update the config. Grafana will only update datasources with the same or lower version number than specified in the config. That way old configs cannot overwrite newer configs if they restart at the same time.
|
||||||
@ -164,3 +165,20 @@ Secure json data is a map of settings that will be encrypted with [secret key](/
|
|||||||
| tlsClientKey | string | *All* |TLS Client key for outgoing requests |
|
| tlsClientKey | string | *All* |TLS Client key for outgoing requests |
|
||||||
| password | string | Postgre | password |
|
| password | string | Postgre | password |
|
||||||
| user | string | Postgre | user |
|
| user | string | Postgre | user |
|
||||||
|
|
||||||
|
### Dashboards
|
||||||
|
|
||||||
|
It's possible to manage dashboards in Grafana by adding one or more yaml config files in the [`provisioning/dashboards`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `dashboards providers` that will load dashboards into grafana. Currently we only support reading dashboards from file but we will add more providers in the future.
|
||||||
|
|
||||||
|
The dashboard provider config file looks like this
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: 'default'
|
||||||
|
org_id: 1
|
||||||
|
folder: ''
|
||||||
|
type: file
|
||||||
|
options:
|
||||||
|
folder: /var/lib/grafana/dashboards
|
||||||
|
```
|
||||||
|
|
||||||
|
When grafana starts it will update/insert all dashboards available in the configured folders. If you modify the file the dashboard will also be updated.
|
@ -126,30 +126,31 @@ There are couple of configurations options which need to be set in Grafana UI un
|
|||||||
|
|
||||||
Once these two properties are set, you can send the alerts to Kafka for further processing or throttling them.
|
Once these two properties are set, you can send the alerts to Kafka for further processing or throttling them.
|
||||||
|
|
||||||
### Other Supported Notification Channels
|
### All supported notifier
|
||||||
|
|
||||||
Grafana also supports the following Notification Channels:
|
Name | Type |Support images
|
||||||
|
-----|------------ | ------
|
||||||
|
Slack | `slack` | yes
|
||||||
|
Pagerduty | `pagerduty` | yes
|
||||||
|
Email | `email` | yes
|
||||||
|
Webhook | `webhook` | link
|
||||||
|
Kafka | `kafka` | no
|
||||||
|
Hipchat | `hipchat` | yes
|
||||||
|
VictorOps | `victorops` | yes
|
||||||
|
Sensu | `sensu` | yes
|
||||||
|
OpsGenie | `opsgenie` | yes
|
||||||
|
Threema | `threema` | yes
|
||||||
|
Pushover | `pushover` | no
|
||||||
|
Telegram | `telegram` | no
|
||||||
|
Line | `line` | no
|
||||||
|
Prometheus Alertmanager | `prometheus-alertmanager` | no
|
||||||
|
|
||||||
- HipChat
|
|
||||||
|
|
||||||
- VictorOps
|
|
||||||
|
|
||||||
- Sensu
|
|
||||||
|
|
||||||
- OpsGenie
|
|
||||||
|
|
||||||
- Threema
|
|
||||||
|
|
||||||
- Pushover
|
|
||||||
|
|
||||||
- Telegram
|
|
||||||
|
|
||||||
- LINE
|
|
||||||
|
|
||||||
# Enable images in notifications {#external-image-store}
|
# Enable images in notifications {#external-image-store}
|
||||||
|
|
||||||
Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessible (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports
|
Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessible (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports
|
||||||
Amazon S3 and Webdav for this. So to set that up you need to configure the [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini config file.
|
Amazon S3, Webdav, and Azure Blob Storage for this. So to set that up you need to configure the [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini config file.
|
||||||
|
|
||||||
Currently only the Email Channels attaches images if no external image store is specified. To include images in alert notifications for other channels then you need to set up an external image store.
|
Currently only the Email Channels attaches images if no external image store is specified. To include images in alert notifications for other channels then you need to set up an external image store.
|
||||||
|
|
||||||
|
@ -78,11 +78,14 @@ CloudWatch Datasource Plugin provides the following queries you can specify in t
|
|||||||
edit view. They allow you to fill a variable's options list with things like `region`, `namespaces`, `metric names`
|
edit view. They allow you to fill a variable's options list with things like `region`, `namespaces`, `metric names`
|
||||||
and `dimension keys/values`.
|
and `dimension keys/values`.
|
||||||
|
|
||||||
|
In place of `region` you can specify `default` to use the default region configured in the datasource for the query,
|
||||||
|
e.g. `metrics(AWS/DynamoDB, default)` or `dimension_values(default, ..., ..., ...)`.
|
||||||
|
|
||||||
Name | Description
|
Name | Description
|
||||||
------- | --------
|
------- | --------
|
||||||
*regions()* | Returns a list of regions AWS provides their service.
|
*regions()* | Returns a list of regions AWS provides their service.
|
||||||
*namespaces()* | Returns a list of namespaces CloudWatch support.
|
*namespaces()* | Returns a list of namespaces CloudWatch support.
|
||||||
*metrics(namespace, [region])* | Returns a list of metrics in the namespace. (specify region for custom metrics)
|
*metrics(namespace, [region])* | Returns a list of metrics in the namespace. (specify region or use "default" for custom metrics)
|
||||||
*dimension_keys(namespace)* | Returns a list of dimension keys in the namespace.
|
*dimension_keys(namespace)* | Returns a list of dimension keys in the namespace.
|
||||||
*dimension_values(region, namespace, metric, dimension_key)* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`.
|
*dimension_values(region, namespace, metric, dimension_key)* | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`.
|
||||||
*ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`.
|
*ebs_volume_ids(region, instance_id)* | Returns a list of volume ids matching the specified `region`, `instance_id`.
|
||||||
|
@ -45,7 +45,14 @@ To simplify syntax and to allow for dynamic parts, like date range filters, the
|
|||||||
|
|
||||||
Macro example | Description
|
Macro example | Description
|
||||||
------------ | -------------
|
------------ | -------------
|
||||||
|
*$__time(dateColumn)* | Will be replaced by an expression to convert to a UNIX timestamp and rename the column to `time_sec`. For example, *UNIX_TIMESTAMP(dateColumn) as time_sec*
|
||||||
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)*
|
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > FROM_UNIXTIME(1494410783) AND dateColumn < FROM_UNIXTIME(1494497183)*
|
||||||
|
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *FROM_UNIXTIME(1494410783)*
|
||||||
|
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *FROM_UNIXTIME(1494497183)*
|
||||||
|
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed) as time_sec,*
|
||||||
|
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
|
||||||
|
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
|
||||||
|
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
|
||||||
|
|
||||||
We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
|
We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo.
|
||||||
|
|
||||||
@ -99,6 +106,19 @@ GROUP BY metric1, UNIX_TIMESTAMP(time_date_time) DIV 300
|
|||||||
ORDER BY time_sec asc
|
ORDER BY time_sec asc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example with $__timeGroup macro:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
$__timeGroup(time_date_time,'5m') as time_sec,
|
||||||
|
min(value_double) as value,
|
||||||
|
metric_name as metric
|
||||||
|
FROM test_data
|
||||||
|
WHERE $__timeFilter(time_date_time)
|
||||||
|
GROUP BY 1, metric_name
|
||||||
|
ORDER BY 1
|
||||||
|
```
|
||||||
|
|
||||||
Currently, there is no support for a dynamic group by time based on time range & panel width.
|
Currently, there is no support for a dynamic group by time based on time range & panel width.
|
||||||
This is something we plan to add.
|
This is something we plan to add.
|
||||||
|
|
||||||
@ -127,6 +147,12 @@ A query can returns multiple columns and Grafana will automatically create a lis
|
|||||||
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
|
SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
|
||||||
|
```
|
||||||
|
|
||||||
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
|
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
@ -48,7 +48,7 @@ Macro example | Description
|
|||||||
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *extract(epoch from dateColumn) BETWEEN 1494410783 AND 1494497183*
|
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *extract(epoch from dateColumn) BETWEEN 1494410783 AND 1494497183*
|
||||||
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)*
|
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)*
|
||||||
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)*
|
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)*
|
||||||
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from "dateColumn")/300)::bigint*300*
|
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time*
|
||||||
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
|
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
|
||||||
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
|
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
|
||||||
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
|
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
|
||||||
@ -94,7 +94,7 @@ Example with `metric` column
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
$__timeGroup(time_date_time,'5m') as time,
|
$__timeGroup(time_date_time,'5m'),
|
||||||
min(value_double),
|
min(value_double),
|
||||||
'min' as metric
|
'min' as metric
|
||||||
FROM test_data
|
FROM test_data
|
||||||
@ -107,7 +107,7 @@ Example with multiple columns:
|
|||||||
|
|
||||||
```sql
|
```sql
|
||||||
SELECT
|
SELECT
|
||||||
$__timeGroup(time_date_time,'5m') as time,
|
$__timeGroup(time_date_time,'5m'),
|
||||||
min(value_double) as min_value,
|
min(value_double) as min_value,
|
||||||
max(value_double) as max_value
|
max(value_double) as max_value
|
||||||
FROM test_data
|
FROM test_data
|
||||||
@ -139,6 +139,12 @@ A query can return multiple columns and Grafana will automatically create a list
|
|||||||
SELECT host.hostname, other_host.hostname2 FROM host JOIN other_host ON host.city = other_host.city
|
SELECT host.hostname, other_host.hostname2 FROM host JOIN other_host ON host.city = other_host.city
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*.
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT event_name FROM event_log WHERE $__timeFilter(time_column)
|
||||||
|
```
|
||||||
|
|
||||||
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
|
Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
@ -47,7 +47,7 @@ The coloring options of the Singlestat Panel config allow you to dynamically cha
|
|||||||
2. **Thresholds**: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90.
|
2. **Thresholds**: Change the background and value colors dynamically within the panel, depending on the Singlestat value. The threshold field accepts **2 comma-separated** values which represent 3 ranges that correspond to the three colors directly to the right. For example: if the thresholds are 70, 90 then the first color represents < 70, the second color represents between 70 and 90 and the third color represents > 90.
|
||||||
3. **Colors**: Select a color and opacity
|
3. **Colors**: Select a color and opacity
|
||||||
4. **Value**: This checkbox applies the configured thresholds and colors to the summary stat.
|
4. **Value**: This checkbox applies the configured thresholds and colors to the summary stat.
|
||||||
5. **Invert order**: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/docs(v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/docs/v1/ryg.png">).
|
5. **Invert order**: This link toggles the threshold color order.</br>For example: Green, Orange, Red (<img class="no-shadow" src="/img/docs/v1/gyr.png">) will become Red, Orange, Green (<img class="no-shadow" src="/img/docs/v1/ryg.png">).
|
||||||
|
|
||||||
### Spark Lines
|
### Spark Lines
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ of another alert in your conditions, and `Time Of Day`.
|
|||||||
Alerting would not be very useful if there was no way to send notifications when rules trigger and change state. You
|
Alerting would not be very useful if there was no way to send notifications when rules trigger and change state. You
|
||||||
can setup notifications of different types. We currently have `Slack`, `PagerDuty`, `Email` and `Webhook` with more in the
|
can setup notifications of different types. We currently have `Slack`, `PagerDuty`, `Email` and `Webhook` with more in the
|
||||||
pipe that will be added during beta period. The notifications can then be added to your alert rules.
|
pipe that will be added during beta period. The notifications can then be added to your alert rules.
|
||||||
If you have configured an external image store in the grafana.ini config file (s3 and webdav options available)
|
If you have configured an external image store in the grafana.ini config file (s3, webdav, and azure_blob options available)
|
||||||
you can get very rich notifications with an image of the graph and the metric
|
you can get very rich notifications with an image of the graph and the metric
|
||||||
values all included in the notification.
|
values all included in the notification.
|
||||||
|
|
||||||
|
@ -196,6 +196,8 @@ Content-Type: application/json
|
|||||||
|
|
||||||
## Create alert notification
|
## Create alert notification
|
||||||
|
|
||||||
|
You can find the full list of [supported notifers](/alerting/notifications/#all-supported-notifier) at the alert notifiers page.
|
||||||
|
|
||||||
`POST /api/alert-notifications`
|
`POST /api/alert-notifications`
|
||||||
|
|
||||||
**Example Request**:
|
**Example Request**:
|
||||||
|
@ -100,7 +100,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
|||||||
JSON Body schema:
|
JSON Body schema:
|
||||||
|
|
||||||
- **name** – The key name
|
- **name** – The key name
|
||||||
- **role** – Sets the access level/Grafana Role for the key. Can be one of the following values: `Viewer`, `Editor`, `Read Only Editor` or `Admin`.
|
- **role** – Sets the access level/Grafana Role for the key. Can be one of the following values: `Viewer`, `Editor` or `Admin`.
|
||||||
|
|
||||||
**Example Response**:
|
**Example Response**:
|
||||||
|
|
||||||
|
@ -156,7 +156,7 @@ HTTP/1.1 200
|
|||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
|
|
||||||
{
|
{
|
||||||
"email": "user@mygraf.com"
|
"email": "user@mygraf.com",
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
"login": "admin",
|
"login": "admin",
|
||||||
"theme": "light",
|
"theme": "light",
|
||||||
|
@ -91,9 +91,11 @@ file.
|
|||||||
|
|
||||||
Directory where grafana will automatically scan and look for plugins
|
Directory where grafana will automatically scan and look for plugins
|
||||||
|
|
||||||
### datasources
|
### provisioning
|
||||||
|
|
||||||
Config files containing datasources that will be configured at startup
|
> This feature is available in 5.0+
|
||||||
|
|
||||||
|
Folder that contains [provisioning](/administration/provisioning) config files that grafana will apply on startup. Dashboards will be reloaded when the json files changes
|
||||||
|
|
||||||
## [server]
|
## [server]
|
||||||
|
|
||||||
@ -203,7 +205,7 @@ The database user (not applicable for `sqlite3`).
|
|||||||
|
|
||||||
### password
|
### password
|
||||||
|
|
||||||
The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with trippel quotes. Ex `"""#password;"""`
|
The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with triple quotes. Ex `"""#password;"""`
|
||||||
|
|
||||||
### ssl_mode
|
### ssl_mode
|
||||||
|
|
||||||
@ -212,19 +214,19 @@ For MySQL, use either `true`, `false`, or `skip-verify`.
|
|||||||
|
|
||||||
### ca_cert_path
|
### ca_cert_path
|
||||||
|
|
||||||
(MySQL only) The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`.
|
The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`.
|
||||||
|
|
||||||
### client_key_path
|
### client_key_path
|
||||||
|
|
||||||
(MySQL only) The path to the client key. Only if server requires client authentication.
|
The path to the client key. Only if server requires client authentication.
|
||||||
|
|
||||||
### client_cert_path
|
### client_cert_path
|
||||||
|
|
||||||
(MySQL only) The path to the client cert. Only if server requires client authentication.
|
The path to the client cert. Only if server requires client authentication.
|
||||||
|
|
||||||
### server_cert_name
|
### server_cert_name
|
||||||
|
|
||||||
(MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`.
|
The common name field of the certificate used by the `mysql` or `postgres` server. Not necessary if `ssl_mode` is set to `skip-verify`.
|
||||||
|
|
||||||
### max_idle_conn
|
### max_idle_conn
|
||||||
The maximum number of connections in the idle connection pool.
|
The maximum number of connections in the idle connection pool.
|
||||||
@ -290,10 +292,14 @@ organization to be created for that new user.
|
|||||||
|
|
||||||
The role new users will be assigned for the main organization (if the
|
The role new users will be assigned for the main organization (if the
|
||||||
above setting is set to true). Defaults to `Viewer`, other valid
|
above setting is set to true). Defaults to `Viewer`, other valid
|
||||||
options are `Admin` and `Editor` and `Read Only Editor`. e.g. :
|
options are `Admin` and `Editor`. e.g. :
|
||||||
|
|
||||||
`auto_assign_org_role = Read Only Editor`
|
`auto_assign_org_role = Viewer`
|
||||||
|
|
||||||
|
### viewers can edit
|
||||||
|
|
||||||
|
Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
|
||||||
|
Defaults to `false`.
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
@ -632,8 +638,7 @@ Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1.
|
|||||||
|
|
||||||
## [dashboards.json]
|
## [dashboards.json]
|
||||||
|
|
||||||
If you have a system that automatically builds dashboards as json files you can enable this feature to have the
|
> This have been replaced with dashboards [provisioning](/administration/provisioning) in 5.0+
|
||||||
Grafana backend index those json dashboards which will make them appear in regular dashboard search.
|
|
||||||
|
|
||||||
### enabled
|
### enabled
|
||||||
`true` or `false`. Is disabled by default.
|
`true` or `false`. Is disabled by default.
|
||||||
@ -726,7 +731,7 @@ Time to live for snapshots.
|
|||||||
These options control how images should be made public so they can be shared on services like slack.
|
These options control how images should be made public so they can be shared on services like slack.
|
||||||
|
|
||||||
### provider
|
### provider
|
||||||
You can choose between (s3, webdav, gcs). If left empty Grafana will ignore the upload action.
|
You can choose between (s3, webdav, gcs, azure_blob). If left empty Grafana will ignore the upload action.
|
||||||
|
|
||||||
## [external_image_storage.s3]
|
## [external_image_storage.s3]
|
||||||
|
|
||||||
@ -781,6 +786,17 @@ Bucket Name on Google Cloud Storage.
|
|||||||
### path
|
### path
|
||||||
Optional extra path inside bucket
|
Optional extra path inside bucket
|
||||||
|
|
||||||
|
## [external_image_storage.azure_blob]
|
||||||
|
|
||||||
|
### account_name
|
||||||
|
Storage account name
|
||||||
|
|
||||||
|
### account_key
|
||||||
|
Storage account key
|
||||||
|
|
||||||
|
### container_name
|
||||||
|
Container name where to store "Blob" images with random names. Creating the blob container beforehand is required. Only public containers are supported.
|
||||||
|
|
||||||
## [alerting]
|
## [alerting]
|
||||||
|
|
||||||
### enabled
|
### enabled
|
||||||
|
@ -15,9 +15,7 @@ weight = 1
|
|||||||
|
|
||||||
Description | Download
|
Description | Download
|
||||||
------------ | -------------
|
------------ | -------------
|
||||||
Stable for Debian-based Linux | [grafana_4.6.2_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.2_amd64.deb)
|
Stable for Debian-based Linux | [grafana_4.6.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb)
|
||||||
|
|
||||||
<!-- Beta for Debian-based Linux | [grafana_4.5.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.5.0-beta1_amd64.deb) -->
|
|
||||||
|
|
||||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||||
installation.
|
installation.
|
||||||
@ -26,21 +24,10 @@ installation.
|
|||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.2_amd64.deb
|
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.6.3_amd64.deb
|
||||||
sudo apt-get install -y adduser libfontconfig
|
sudo apt-get install -y adduser libfontconfig
|
||||||
sudo dpkg -i grafana_4.6.2_amd64.deb
|
sudo dpkg -i grafana_4.6.3_amd64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
<!--
|
|
||||||
## Install Latest Beta
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.5.2-beta1_amd64.deb
|
|
||||||
sudo apt-get install -y adduser libfontconfig
|
|
||||||
sudo dpkg -i grafana_4.5.2-beta1_amd64.deb
|
|
||||||
```
|
|
||||||
-->
|
|
||||||
|
|
||||||
## APT Repository
|
## APT Repository
|
||||||
|
|
||||||
Add the following line to your `/etc/apt/sources.list` file.
|
Add the following line to your `/etc/apt/sources.list` file.
|
||||||
|
@ -15,9 +15,7 @@ weight = 2
|
|||||||
|
|
||||||
Description | Download
|
Description | Download
|
||||||
------------ | -------------
|
------------ | -------------
|
||||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.2 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm)
|
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.6.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm)
|
||||||
|
|
||||||
<!-- Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [4.5.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.5.0-beta1.x86_64.rpm) -->
|
|
||||||
|
|
||||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||||
installation.
|
installation.
|
||||||
@ -27,7 +25,7 @@ installation.
|
|||||||
You can install Grafana using Yum directly.
|
You can install Grafana using Yum directly.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm
|
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
Or install manually using `rpm`.
|
Or install manually using `rpm`.
|
||||||
@ -35,15 +33,15 @@ Or install manually using `rpm`.
|
|||||||
#### On CentOS / Fedora / Redhat:
|
#### On CentOS / Fedora / Redhat:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2-1.x86_64.rpm
|
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3-1.x86_64.rpm
|
||||||
$ sudo yum install initscripts fontconfig
|
$ sudo yum install initscripts fontconfig
|
||||||
$ sudo rpm -Uvh grafana-4.6.2-1.x86_64.rpm
|
$ sudo rpm -Uvh grafana-4.6.3-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
#### On OpenSuse:
|
#### On OpenSuse:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ sudo rpm -i --nodeps grafana-4.6.2-1.x86_64.rpm
|
$ sudo rpm -i --nodeps grafana-4.6.3-1.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install via YUM Repository
|
## Install via YUM Repository
|
||||||
|
@ -13,7 +13,7 @@ weight = 3
|
|||||||
|
|
||||||
Description | Download
|
Description | Download
|
||||||
------------ | -------------
|
------------ | -------------
|
||||||
Latest stable package for Windows | [grafana.4.6.2.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.2.windows-x64.zip)
|
Latest stable package for Windows | [grafana.4.6.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.6.3.windows-x64.zip)
|
||||||
|
|
||||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||||
installation.
|
installation.
|
||||||
|
@ -71,8 +71,8 @@ Each field in the dashboard JSON is explained below with its usage:
|
|||||||
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
|
| **timepicker** | timepicker metadata, see [timepicker section](#timepicker) for details |
|
||||||
| **templating** | templating metadata, see [templating section](#templating) for details |
|
| **templating** | templating metadata, see [templating section](#templating) for details |
|
||||||
| **annotations** | annotations metadata, see [annotations section](#annotations) for details |
|
| **annotations** | annotations metadata, see [annotations section](#annotations) for details |
|
||||||
| **schemaVersion** | TODO |
|
| **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to the said schema |
|
||||||
| **version** | TODO |
|
| **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
|
||||||
| **links** | TODO |
|
| **links** | TODO |
|
||||||
|
|
||||||
### rows
|
### rows
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
verbose: true,
|
verbose: false,
|
||||||
"globals": {
|
"globals": {
|
||||||
"ts-jest": {
|
"ts-jest": {
|
||||||
"tsConfigFile": "tsconfig.json"
|
"tsConfigFile": "tsconfig.json"
|
||||||
|
45
package.json
45
package.json
@ -4,7 +4,7 @@
|
|||||||
"company": "Grafana Labs"
|
"company": "Grafana Labs"
|
||||||
},
|
},
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "4.7.0-pre1",
|
"version": "5.0.0-pre1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
@ -14,8 +14,8 @@
|
|||||||
"@types/enzyme": "^2.8.9",
|
"@types/enzyme": "^2.8.9",
|
||||||
"@types/jest": "^21.1.4",
|
"@types/jest": "^21.1.4",
|
||||||
"@types/node": "^8.0.31",
|
"@types/node": "^8.0.31",
|
||||||
"@types/react": "^16.0.5",
|
"@types/react": "^16.0.25",
|
||||||
"@types/react-dom": "^15.5.4",
|
"@types/react-dom": "^16.0.3",
|
||||||
"angular-mocks": "^1.6.6",
|
"angular-mocks": "^1.6.6",
|
||||||
"autoprefixer": "^6.4.0",
|
"autoprefixer": "^6.4.0",
|
||||||
"awesome-typescript-loader": "^3.2.3",
|
"awesome-typescript-loader": "^3.2.3",
|
||||||
@ -65,7 +65,7 @@
|
|||||||
"karma-sinon": "^1.0.5",
|
"karma-sinon": "^1.0.5",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "^0.3.7",
|
||||||
"karma-webpack": "^2.0.4",
|
"karma-webpack": "^2.0.4",
|
||||||
"lint-staged": "^4.2.3",
|
"lint-staged": "^6.0.0",
|
||||||
"load-grunt-tasks": "3.5.2",
|
"load-grunt-tasks": "3.5.2",
|
||||||
"mocha": "^4.0.1",
|
"mocha": "^4.0.1",
|
||||||
"ng-annotate-loader": "^0.6.1",
|
"ng-annotate-loader": "^0.6.1",
|
||||||
@ -76,7 +76,7 @@
|
|||||||
"postcss-browser-reporter": "^0.5.0",
|
"postcss-browser-reporter": "^0.5.0",
|
||||||
"postcss-loader": "^2.0.6",
|
"postcss-loader": "^2.0.6",
|
||||||
"postcss-reporter": "^5.0.0",
|
"postcss-reporter": "^5.0.0",
|
||||||
"prettier": "1.7.3",
|
"prettier": "1.9.2",
|
||||||
"react-test-renderer": "^16.0.0",
|
"react-test-renderer": "^16.0.0",
|
||||||
"sass-lint": "^1.10.2",
|
"sass-lint": "^1.10.2",
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
@ -103,7 +103,22 @@
|
|||||||
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
|
"lint": "tslint -c tslint.json --project tsconfig.json --type-check",
|
||||||
"karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
|
"karma": "node ./node_modules/grunt-cli/bin/grunt karma:dev",
|
||||||
"jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
|
"jest": "node ./node_modules/jest-cli/bin/jest.js --notify --watch",
|
||||||
"precommit": "node ./node_modules/grunt-cli/bin/grunt precommit"
|
"precommit": "lint-staged && node ./node_modules/grunt-cli/bin/grunt precommit"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx}": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
],
|
||||||
|
"*.scss": [
|
||||||
|
"prettier --write",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -115,22 +130,26 @@
|
|||||||
"angular-sanitize": "^1.6.6",
|
"angular-sanitize": "^1.6.6",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"brace": "^0.10.0",
|
"brace": "^0.10.0",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
"clipboard": "^1.7.1",
|
"clipboard": "^1.7.1",
|
||||||
"eventemitter3": "^2.0.3",
|
"d3": "^4.11.0",
|
||||||
|
"d3-scale-chromatic": "^1.1.1",
|
||||||
|
"eventemitter3": "^2.0.2",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
"jquery": "^3.2.1",
|
"jquery": "^3.2.1",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
"mousetrap": "^1.6.0",
|
"mousetrap": "^1.6.0",
|
||||||
"ngreact": "^0.4.1",
|
"perfect-scrollbar": "^1.2.0",
|
||||||
"react": "^16.0.0",
|
"prop-types": "^15.6.0",
|
||||||
"react-dom": "^16.0.0",
|
"react": "^16.1.1",
|
||||||
|
"react-dom": "^16.1.1",
|
||||||
|
"react-grid-layout": "^0.16.1",
|
||||||
|
"react-sizeme": "^2.3.6",
|
||||||
"remarkable": "^1.7.1",
|
"remarkable": "^1.7.1",
|
||||||
"rxjs": "^5.4.3",
|
"rxjs": "^5.4.3",
|
||||||
"tether": "^1.4.0",
|
"tether": "^1.4.0",
|
||||||
"tether-drop": "https://github.com/torkelo/drop",
|
"tether-drop": "https://github.com/torkelo/drop",
|
||||||
"tinycolor2": "^1.4.1",
|
"tinycolor2": "^1.4.1"
|
||||||
"d3": "^4.11.0",
|
|
||||||
"d3-scale-chromatic": "^1.1.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,12 @@ case "$1" in
|
|||||||
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -f $PROVISIONING_CFG_DIR ]; then
|
||||||
|
mkdir -p $PROVISIONING_CFG_DIR/dashboards $PROVISIONING_CFG_DIR/datasources
|
||||||
|
cp /usr/share/grafana/conf/provisioning/dashboards/sample.yaml $PROVISIONING_CFG_DIR/dashboards/sample.yaml
|
||||||
|
cp /usr/share/grafana/conf/provisioning/datasources/sample.yaml $PROVISIONING_CFG_DIR/datasources/sample.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
# configuration files should not be modifiable by grafana user, as this can be a security issue
|
# configuration files should not be modifiable by grafana user, as this can be a security issue
|
||||||
chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
|
chown -Rh root:$GRAFANA_GROUP /etc/grafana/*
|
||||||
chmod 755 /etc/grafana
|
chmod 755 /etc/grafana
|
||||||
|
@ -18,5 +18,7 @@ RESTART_ON_UPGRADE=true
|
|||||||
|
|
||||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||||
|
|
||||||
|
PROVISIONING_CFG_DIR=/etc/grafana/provisioning
|
||||||
|
|
||||||
# Only used on systemd systems
|
# Only used on systemd systems
|
||||||
PID_FILE_DIR=/var/run/grafana
|
PID_FILE_DIR=/var/run/grafana
|
||||||
|
@ -33,6 +33,7 @@ DATA_DIR=/var/lib/grafana
|
|||||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||||
LOG_DIR=/var/log/grafana
|
LOG_DIR=/var/log/grafana
|
||||||
CONF_FILE=$CONF_DIR/grafana.ini
|
CONF_FILE=$CONF_DIR/grafana.ini
|
||||||
|
PROVISIONING_CFG_DIR=$CONF_DIR/provisioning
|
||||||
MAX_OPEN_FILES=10000
|
MAX_OPEN_FILES=10000
|
||||||
PID_FILE=/var/run/$NAME.pid
|
PID_FILE=/var/run/$NAME.pid
|
||||||
DAEMON=/usr/sbin/$NAME
|
DAEMON=/usr/sbin/$NAME
|
||||||
@ -55,7 +56,7 @@ if [ -f "$DEFAULT" ]; then
|
|||||||
. "$DEFAULT"
|
. "$DEFAULT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||||
|
|
||||||
function checkUser() {
|
function checkUser() {
|
||||||
if [ `id -u` -ne 0 ]; then
|
if [ `id -u` -ne 0 ]; then
|
||||||
|
@ -19,7 +19,10 @@ ExecStart=/usr/sbin/grafana-server \
|
|||||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||||
cfg:default.paths.logs=${LOG_DIR} \
|
cfg:default.paths.logs=${LOG_DIR} \
|
||||||
cfg:default.paths.data=${DATA_DIR} \
|
cfg:default.paths.data=${DATA_DIR} \
|
||||||
cfg:default.paths.plugins=${PLUGINS_DIR}
|
cfg:default.paths.plugins=${PLUGINS_DIR} \
|
||||||
|
cfg:default.paths.provisioning=${PROVISIONING_CFG_DIR}
|
||||||
|
|
||||||
|
|
||||||
LimitNOFILE=10000
|
LimitNOFILE=10000
|
||||||
TimeoutStopSec=20
|
TimeoutStopSec=20
|
||||||
UMask=0027
|
UMask=0027
|
||||||
|
@ -6,10 +6,12 @@ HOMEPATH=/usr/local/share/grafana
|
|||||||
LOGPATH=/usr/local/var/log/grafana
|
LOGPATH=/usr/local/var/log/grafana
|
||||||
DATAPATH=/usr/local/var/lib/grafana
|
DATAPATH=/usr/local/var/lib/grafana
|
||||||
PLUGINPATH=/usr/local/var/lib/grafana/plugins
|
PLUGINPATH=/usr/local/var/lib/grafana/plugins
|
||||||
|
DATASOURCECFGPATH=/usr/local/etc/grafana/datasources
|
||||||
|
DASHBOARDSCFGPATH=/usr/local/etc/grafana/dashboards
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
$EXECUTABLE --config=$CONFIG --homepath=$HOMEPATH cfg:default.paths.logs=$LOGPATH cfg:default.paths.data=$DATAPATH cfg:default.paths.plugins=$PLUGINPATH 2> /dev/null &
|
$EXECUTABLE --config=$CONFIG --homepath=$HOMEPATH cfg:default.paths.datasources=$DATASOURCECFGPATH cfg:default.paths.dashboards=$DASHBOARDSCFGPATH cfg:default.paths.logs=$LOGPATH cfg:default.paths.data=$DATAPATH cfg:default.paths.plugins=$PLUGINPATH 2> /dev/null &
|
||||||
[ $? -eq 0 ] && echo "$DAEMON started"
|
[ $? -eq 0 ] && echo "$DAEMON started"
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#! /usr/bin/env bash
|
#! /usr/bin/env bash
|
||||||
version=4.6.2
|
version=4.6.3
|
||||||
|
|
||||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb
|
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb
|
||||||
|
|
||||||
|
@ -45,6 +45,12 @@ if [ $1 -eq 1 ] ; then
|
|||||||
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
cp /usr/share/grafana/conf/ldap.toml /etc/grafana/ldap.toml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -f $PROVISIONING_CFG_DIR ]; then
|
||||||
|
mkdir -p $PROVISIONING_CFG_DIR/dashboards $PROVISIONING_CFG_DIR/datasources
|
||||||
|
cp /usr/share/grafana/conf/provisioning/dashboards/sample.yaml $PROVISIONING_CFG_DIR/dashboards/sample.yaml
|
||||||
|
cp /usr/share/grafana/conf/provisioning/datasources/sample.yaml $PROVISIONING_CFG_DIR/datasources/sample.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
# Set user permissions on /var/log/grafana, /var/lib/grafana
|
# Set user permissions on /var/log/grafana, /var/lib/grafana
|
||||||
mkdir -p /var/log/grafana /var/lib/grafana
|
mkdir -p /var/log/grafana /var/lib/grafana
|
||||||
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana
|
chown -R $GRAFANA_USER:$GRAFANA_GROUP /var/log/grafana /var/lib/grafana
|
||||||
|
@ -32,6 +32,7 @@ DATA_DIR=/var/lib/grafana
|
|||||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||||
LOG_DIR=/var/log/grafana
|
LOG_DIR=/var/log/grafana
|
||||||
CONF_FILE=$CONF_DIR/grafana.ini
|
CONF_FILE=$CONF_DIR/grafana.ini
|
||||||
|
PROVISIONING_CFG_DIR=$CONF_DIR/provisioning
|
||||||
MAX_OPEN_FILES=10000
|
MAX_OPEN_FILES=10000
|
||||||
PID_FILE=/var/run/$NAME.pid
|
PID_FILE=/var/run/$NAME.pid
|
||||||
DAEMON=/usr/sbin/$NAME
|
DAEMON=/usr/sbin/$NAME
|
||||||
@ -59,7 +60,7 @@ fi
|
|||||||
# overwrite settings from default file
|
# overwrite settings from default file
|
||||||
[ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
|
[ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME
|
||||||
|
|
||||||
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
|
||||||
|
|
||||||
function isRunning() {
|
function isRunning() {
|
||||||
status -p $PID_FILE $NAME > /dev/null 2>&1
|
status -p $PID_FILE $NAME > /dev/null 2>&1
|
||||||
|
@ -18,5 +18,7 @@ RESTART_ON_UPGRADE=true
|
|||||||
|
|
||||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||||
|
|
||||||
|
PROVISIONING_CFG_DIR=/etc/grafana/provisioning
|
||||||
|
|
||||||
# Only used on systemd systems
|
# Only used on systemd systems
|
||||||
PID_FILE_DIR=/var/run/grafana
|
PID_FILE_DIR=/var/run/grafana
|
||||||
|
@ -9,7 +9,7 @@ After=postgresql.service mariadb.service mysql.service
|
|||||||
EnvironmentFile=/etc/sysconfig/grafana-server
|
EnvironmentFile=/etc/sysconfig/grafana-server
|
||||||
User=grafana
|
User=grafana
|
||||||
Group=grafana
|
Group=grafana
|
||||||
Type=simple
|
Type=notify
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
WorkingDirectory=/usr/share/grafana
|
WorkingDirectory=/usr/share/grafana
|
||||||
RuntimeDirectory=grafana
|
RuntimeDirectory=grafana
|
||||||
@ -19,7 +19,9 @@ ExecStart=/usr/sbin/grafana-server \
|
|||||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||||
cfg:default.paths.logs=${LOG_DIR} \
|
cfg:default.paths.logs=${LOG_DIR} \
|
||||||
cfg:default.paths.data=${DATA_DIR} \
|
cfg:default.paths.data=${DATA_DIR} \
|
||||||
cfg:default.paths.plugins=${PLUGINS_DIR}
|
cfg:default.paths.plugins=${PLUGINS_DIR} \
|
||||||
|
cfg:default.paths.provisioning=${PROVISIONING_CFG_DIR}
|
||||||
|
|
||||||
LimitNOFILE=10000
|
LimitNOFILE=10000
|
||||||
TimeoutStopSec=20
|
TimeoutStopSec=20
|
||||||
|
|
||||||
|
@ -40,9 +40,14 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
r.Get("/datasources/", reqSignedIn, Index)
|
r.Get("/datasources/", reqSignedIn, Index)
|
||||||
r.Get("/datasources/new", reqSignedIn, Index)
|
r.Get("/datasources/new", reqSignedIn, Index)
|
||||||
r.Get("/datasources/edit/*", reqSignedIn, Index)
|
r.Get("/datasources/edit/*", reqSignedIn, Index)
|
||||||
r.Get("/org/users/", reqSignedIn, Index)
|
r.Get("/org/users", reqSignedIn, Index)
|
||||||
|
r.Get("/org/users/new", reqSignedIn, Index)
|
||||||
|
r.Get("/org/users/invite", reqSignedIn, Index)
|
||||||
|
r.Get("/org/teams", reqSignedIn, Index)
|
||||||
|
r.Get("/org/teams/*", reqSignedIn, Index)
|
||||||
r.Get("/org/apikeys/", reqSignedIn, Index)
|
r.Get("/org/apikeys/", reqSignedIn, Index)
|
||||||
r.Get("/dashboard/import/", reqSignedIn, Index)
|
r.Get("/dashboard/import/", reqSignedIn, Index)
|
||||||
|
r.Get("/configuration", reqGrafanaAdmin, Index)
|
||||||
r.Get("/admin", reqGrafanaAdmin, Index)
|
r.Get("/admin", reqGrafanaAdmin, Index)
|
||||||
r.Get("/admin/settings", reqGrafanaAdmin, Index)
|
r.Get("/admin/settings", reqGrafanaAdmin, Index)
|
||||||
r.Get("/admin/users", reqGrafanaAdmin, Index)
|
r.Get("/admin/users", reqGrafanaAdmin, Index)
|
||||||
@ -62,6 +67,7 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
r.Get("/dashboard-solo/snapshot/*", Index)
|
r.Get("/dashboard-solo/snapshot/*", Index)
|
||||||
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
||||||
r.Get("/import/dashboard", reqSignedIn, Index)
|
r.Get("/import/dashboard", reqSignedIn, Index)
|
||||||
|
r.Get("/dashboards/", reqSignedIn, Index)
|
||||||
r.Get("/dashboards/*", reqSignedIn, Index)
|
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||||
|
|
||||||
r.Get("/playlists/", reqSignedIn, Index)
|
r.Get("/playlists/", reqSignedIn, Index)
|
||||||
@ -134,6 +140,18 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
|
usersRoute.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
|
||||||
}, reqGrafanaAdmin)
|
}, reqGrafanaAdmin)
|
||||||
|
|
||||||
|
// team (admin permission required)
|
||||||
|
apiRoute.Group("/teams", func(teamsRoute RouteRegister) {
|
||||||
|
teamsRoute.Get("/:teamId", wrap(GetTeamById))
|
||||||
|
teamsRoute.Get("/search", wrap(SearchTeams))
|
||||||
|
teamsRoute.Post("/", quota("teams"), bind(m.CreateTeamCommand{}), wrap(CreateTeam))
|
||||||
|
teamsRoute.Put("/:teamId", bind(m.UpdateTeamCommand{}), wrap(UpdateTeam))
|
||||||
|
teamsRoute.Delete("/:teamId", wrap(DeleteTeamById))
|
||||||
|
teamsRoute.Get("/:teamId/members", wrap(GetTeamMembers))
|
||||||
|
teamsRoute.Post("/:teamId/members", quota("teams"), bind(m.AddTeamMemberCommand{}), wrap(AddTeamMember))
|
||||||
|
teamsRoute.Delete("/:teamId/members/:userId", wrap(RemoveTeamMember))
|
||||||
|
}, reqOrgAdmin)
|
||||||
|
|
||||||
// org information available to all users.
|
// org information available to all users.
|
||||||
apiRoute.Group("/org", func(orgRoute RouteRegister) {
|
apiRoute.Group("/org", func(orgRoute RouteRegister) {
|
||||||
orgRoute.Get("/", wrap(GetOrgCurrent))
|
orgRoute.Get("/", wrap(GetOrgCurrent))
|
||||||
@ -224,20 +242,27 @@ func (hs *HttpServer) registerRoutes() {
|
|||||||
|
|
||||||
// Dashboard
|
// Dashboard
|
||||||
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
|
apiRoute.Group("/dashboards", func(dashboardRoute RouteRegister) {
|
||||||
dashboardRoute.Get("/db/:slug", GetDashboard)
|
dashboardRoute.Get("/db/:slug", wrap(GetDashboard))
|
||||||
dashboardRoute.Delete("/db/:slug", reqEditorRole, DeleteDashboard)
|
dashboardRoute.Delete("/db/:slug", reqEditorRole, wrap(DeleteDashboard))
|
||||||
|
|
||||||
dashboardRoute.Get("/id/:dashboardId/versions", wrap(GetDashboardVersions))
|
|
||||||
dashboardRoute.Get("/id/:dashboardId/versions/:id", wrap(GetDashboardVersion))
|
|
||||||
dashboardRoute.Post("/id/:dashboardId/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
|
||||||
|
|
||||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
|
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), wrap(CalculateDashboardDiff))
|
||||||
|
|
||||||
dashboardRoute.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
|
dashboardRoute.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), wrap(PostDashboard))
|
||||||
dashboardRoute.Get("/file/:file", GetDashboardFromJsonFile)
|
|
||||||
dashboardRoute.Get("/home", wrap(GetHomeDashboard))
|
dashboardRoute.Get("/home", wrap(GetHomeDashboard))
|
||||||
dashboardRoute.Get("/tags", GetDashboardTags)
|
dashboardRoute.Get("/tags", GetDashboardTags)
|
||||||
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
|
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
|
||||||
|
|
||||||
|
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute RouteRegister) {
|
||||||
|
dashIdRoute.Get("/versions", wrap(GetDashboardVersions))
|
||||||
|
dashIdRoute.Get("/versions/:id", wrap(GetDashboardVersion))
|
||||||
|
dashIdRoute.Post("/restore", reqEditorRole, bind(dtos.RestoreDashboardVersionCommand{}), wrap(RestoreDashboardVersion))
|
||||||
|
|
||||||
|
dashIdRoute.Group("/acl", func(aclRoute RouteRegister) {
|
||||||
|
aclRoute.Get("/", wrap(GetDashboardAclList))
|
||||||
|
aclRoute.Post("/", bind(dtos.UpdateDashboardAclCommand{}), wrap(UpdateDashboardAcl))
|
||||||
|
aclRoute.Delete("/:aclId", wrap(DeleteDashboardAcl))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Dashboard snapshots
|
// Dashboard snapshots
|
||||||
|
@ -25,6 +25,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
|
|
||||||
|
gocache "github.com/patrickmn/go-cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
var gravatarSource string
|
var gravatarSource string
|
||||||
@ -92,7 +94,7 @@ func (this *Avatar) Update() (err error) {
|
|||||||
|
|
||||||
type CacheServer struct {
|
type CacheServer struct {
|
||||||
notFound *Avatar
|
notFound *Avatar
|
||||||
cache map[string]*Avatar
|
cache *gocache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
|
func (this *CacheServer) mustInt(r *http.Request, defaultValue int, keys ...string) (v int) {
|
||||||
@ -110,7 +112,9 @@ func (this *CacheServer) Handler(ctx *macaron.Context) {
|
|||||||
|
|
||||||
var avatar *Avatar
|
var avatar *Avatar
|
||||||
|
|
||||||
if avatar, _ = this.cache[hash]; avatar == nil {
|
if obj, exist := this.cache.Get(hash); exist {
|
||||||
|
avatar = obj.(*Avatar)
|
||||||
|
} else {
|
||||||
avatar = New(hash)
|
avatar = New(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +128,7 @@ func (this *CacheServer) Handler(ctx *macaron.Context) {
|
|||||||
if avatar.notFound {
|
if avatar.notFound {
|
||||||
avatar = this.notFound
|
avatar = this.notFound
|
||||||
} else {
|
} else {
|
||||||
this.cache[hash] = avatar
|
this.cache.Add(hash, avatar, gocache.DefaultExpiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Resp.Header().Add("Content-Type", "image/jpeg")
|
ctx.Resp.Header().Add("Content-Type", "image/jpeg")
|
||||||
@ -146,7 +150,7 @@ func NewCacheServer() *CacheServer {
|
|||||||
|
|
||||||
return &CacheServer{
|
return &CacheServer{
|
||||||
notFound: newNotFound(),
|
notFound: newNotFound(),
|
||||||
cache: make(map[string]*Avatar),
|
cache: gocache.New(time.Hour, time.Hour*2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
@ -16,8 +17,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
"github.com/grafana/grafana/pkg/services/search"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
@ -35,23 +35,34 @@ func isDashboardStarredByUser(c *middleware.Context, dashId int64) (bool, error)
|
|||||||
return query.Result, nil
|
return query.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDashboard(c *middleware.Context) {
|
func dashboardGuardianResponse(err error) Response {
|
||||||
slug := strings.ToLower(c.Params(":slug"))
|
|
||||||
|
|
||||||
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
|
|
||||||
err := bus.Dispatch(&query)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JsonApiErr(404, "Dashboard not found", nil)
|
return ApiError(500, "Error while checking dashboard permissions", err)
|
||||||
return
|
} else {
|
||||||
|
return ApiError(403, "Access denied to this dashboard", nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isStarred, err := isDashboardStarredByUser(c, query.Result.Id)
|
func GetDashboard(c *middleware.Context) Response {
|
||||||
if err != nil {
|
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
|
||||||
c.JsonApiErr(500, "Error while checking if dashboard was starred by user", err)
|
if rsp != nil {
|
||||||
return
|
return rsp
|
||||||
}
|
}
|
||||||
|
|
||||||
dash := query.Result
|
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||||
|
if canView, err := guardian.CanView(); err != nil || !canView {
|
||||||
|
fmt.Printf("%v", err)
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
canEdit, _ := guardian.CanEdit()
|
||||||
|
canSave, _ := guardian.CanSave()
|
||||||
|
canAdmin, _ := guardian.CanAdmin()
|
||||||
|
|
||||||
|
isStarred, err := isDashboardStarredByUser(c, dash.Id)
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(500, "Error while checking if dashboard was starred by user", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Finding creator and last updater of the dashboard
|
// Finding creator and last updater of the dashboard
|
||||||
updater, creator := "Anonymous", "Anonymous"
|
updater, creator := "Anonymous", "Anonymous"
|
||||||
@ -62,29 +73,44 @@ func GetDashboard(c *middleware.Context) {
|
|||||||
creator = getUserLogin(dash.CreatedBy)
|
creator = getUserLogin(dash.CreatedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure db version is in sync with json model version
|
meta := dtos.DashboardMeta{
|
||||||
dash.Data.Set("version", dash.Version)
|
|
||||||
|
|
||||||
dto := dtos.DashboardFullWithMeta{
|
|
||||||
Dashboard: dash.Data,
|
|
||||||
Meta: dtos.DashboardMeta{
|
|
||||||
IsStarred: isStarred,
|
IsStarred: isStarred,
|
||||||
Slug: slug,
|
Slug: dash.Slug,
|
||||||
Type: m.DashTypeDB,
|
Type: m.DashTypeDB,
|
||||||
CanStar: c.IsSignedIn,
|
CanStar: c.IsSignedIn,
|
||||||
CanSave: c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR,
|
CanSave: canSave,
|
||||||
CanEdit: canEditDashboard(c.OrgRole),
|
CanEdit: canEdit,
|
||||||
|
CanAdmin: canAdmin,
|
||||||
Created: dash.Created,
|
Created: dash.Created,
|
||||||
Updated: dash.Updated,
|
Updated: dash.Updated,
|
||||||
UpdatedBy: updater,
|
UpdatedBy: updater,
|
||||||
CreatedBy: creator,
|
CreatedBy: creator,
|
||||||
Version: dash.Version,
|
Version: dash.Version,
|
||||||
},
|
HasAcl: dash.HasAcl,
|
||||||
|
IsFolder: dash.IsFolder,
|
||||||
|
FolderId: dash.FolderId,
|
||||||
|
FolderTitle: "Root",
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup folder title
|
||||||
|
if dash.FolderId > 0 {
|
||||||
|
query := m.GetDashboardQuery{Id: dash.FolderId, OrgId: c.OrgId}
|
||||||
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
return ApiError(500, "Dashboard folder could not be read", err)
|
||||||
|
}
|
||||||
|
meta.FolderTitle = query.Result.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure db version is in sync with json model version
|
||||||
|
dash.Data.Set("version", dash.Version)
|
||||||
|
|
||||||
|
dto := dtos.DashboardFullWithMeta{
|
||||||
|
Dashboard: dash.Data,
|
||||||
|
Meta: meta,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(ben): copy this performance metrics logic for the new API endpoints added
|
|
||||||
c.TimeRequest(metrics.M_Api_Dashboard_Get)
|
c.TimeRequest(metrics.M_Api_Dashboard_Get)
|
||||||
c.JSON(200, dto)
|
return Json(200, dto)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserLogin(userId int64) string {
|
func getUserLogin(userId int64) string {
|
||||||
@ -98,24 +124,32 @@ func getUserLogin(userId int64) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDashboard(c *middleware.Context) {
|
func getDashboardHelper(orgId int64, slug string, id int64) (*m.Dashboard, Response) {
|
||||||
slug := c.Params(":slug")
|
query := m.GetDashboardQuery{Slug: slug, Id: id, OrgId: orgId}
|
||||||
|
|
||||||
query := m.GetDashboardQuery{Slug: slug, OrgId: c.OrgId}
|
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
c.JsonApiErr(404, "Dashboard not found", nil)
|
return nil, ApiError(404, "Dashboard not found", err)
|
||||||
return
|
}
|
||||||
|
return query.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := m.DeleteDashboardCommand{Slug: slug, OrgId: c.OrgId}
|
func DeleteDashboard(c *middleware.Context) Response {
|
||||||
|
dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0)
|
||||||
|
if rsp != nil {
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
|
||||||
|
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||||
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := m.DeleteDashboardCommand{OrgId: c.OrgId, Id: dash.Id}
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
c.JsonApiErr(500, "Failed to delete dashboard", err)
|
return ApiError(500, "Failed to delete dashboard", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp = map[string]interface{}{"title": query.Result.Title}
|
var resp = map[string]interface{}{"title": dash.Title}
|
||||||
|
return Json(200, resp)
|
||||||
c.JSON(200, resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
||||||
@ -124,6 +158,15 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
|||||||
|
|
||||||
dash := cmd.GetDashboardModel()
|
dash := cmd.GetDashboardModel()
|
||||||
|
|
||||||
|
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||||
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dash.IsFolder && dash.FolderId > 0 {
|
||||||
|
return ApiError(400, m.ErrDashboardFolderCannotHaveParent.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
// Check if Title is empty
|
// Check if Title is empty
|
||||||
if dash.Title == "" {
|
if dash.Title == "" {
|
||||||
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
|
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
|
||||||
@ -139,17 +182,24 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateAlertsCmd := alerting.ValidateDashboardAlertsCommand{
|
dashItem := &dashboards.SaveDashboardItem{
|
||||||
|
Dashboard: dash,
|
||||||
|
Message: cmd.Message,
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
UserId: c.UserId,
|
UserId: c.UserId,
|
||||||
Dashboard: dash,
|
Overwrite: cmd.Overwrite,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
dashboard, err := dashboards.GetRepository().SaveDashboard(dashItem)
|
||||||
|
|
||||||
|
if err == m.ErrDashboardTitleEmpty {
|
||||||
|
return ApiError(400, m.ErrDashboardTitleEmpty.Error(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == m.ErrDashboardContainsInvalidAlertData {
|
||||||
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bus.Dispatch(&cmd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == m.ErrDashboardWithSameNameExists {
|
if err == m.ErrDashboardWithSameNameExists {
|
||||||
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
return Json(412, util.DynMap{"status": "name-exists", "message": err.Error()})
|
||||||
@ -171,22 +221,12 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
|
|||||||
return ApiError(500, "Failed to save dashboard", err)
|
return ApiError(500, "Failed to save dashboard", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
alertCmd := alerting.UpdateDashboardAlertsCommand{
|
if err == m.ErrDashboardFailedToUpdateAlertData {
|
||||||
OrgId: c.OrgId,
|
return ApiError(500, "Invalid alert data. Cannot save dashboard", err)
|
||||||
UserId: c.UserId,
|
|
||||||
Dashboard: cmd.Result,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bus.Dispatch(&alertCmd); err != nil {
|
|
||||||
return ApiError(500, "Failed to save alerts", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
c.TimeRequest(metrics.M_Api_Dashboard_Save)
|
||||||
return Json(200, util.DynMap{"status": "success", "slug": cmd.Result.Slug, "version": cmd.Result.Version})
|
return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id})
|
||||||
}
|
|
||||||
|
|
||||||
func canEditDashboard(role m.RoleType) bool {
|
|
||||||
return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHomeDashboard(c *middleware.Context) Response {
|
func GetHomeDashboard(c *middleware.Context) Response {
|
||||||
@ -214,7 +254,9 @@ func GetHomeDashboard(c *middleware.Context) Response {
|
|||||||
|
|
||||||
dash := dtos.DashboardFullWithMeta{}
|
dash := dtos.DashboardFullWithMeta{}
|
||||||
dash.Meta.IsHome = true
|
dash.Meta.IsHome = true
|
||||||
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
|
dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR)
|
||||||
|
dash.Meta.FolderTitle = "Root"
|
||||||
|
|
||||||
jsonParser := json.NewDecoder(file)
|
jsonParser := json.NewDecoder(file)
|
||||||
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
|
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
|
||||||
return ApiError(500, "Failed to load home dashboard", err)
|
return ApiError(500, "Failed to load home dashboard", err)
|
||||||
@ -228,55 +270,41 @@ func GetHomeDashboard(c *middleware.Context) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
|
||||||
rows := dash.Get("rows").MustArray()
|
panels := dash.Get("panels").MustArray()
|
||||||
row := simplejson.NewFromAny(rows[0])
|
|
||||||
|
|
||||||
newpanel := simplejson.NewFromAny(map[string]interface{}{
|
newpanel := simplejson.NewFromAny(map[string]interface{}{
|
||||||
"type": "gettingstarted",
|
"type": "gettingstarted",
|
||||||
"id": 123123,
|
"id": 123123,
|
||||||
"span": 12,
|
"gridPos": map[string]interface{}{
|
||||||
|
"x": 0,
|
||||||
|
"y": 3,
|
||||||
|
"w": 24,
|
||||||
|
"h": 4,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
panels := row.Get("panels").MustArray()
|
|
||||||
panels = append(panels, newpanel)
|
panels = append(panels, newpanel)
|
||||||
row.Set("panels", panels)
|
dash.Set("panels", panels)
|
||||||
}
|
|
||||||
|
|
||||||
func GetDashboardFromJsonFile(c *middleware.Context) {
|
|
||||||
file := c.Params(":file")
|
|
||||||
|
|
||||||
dashboard := search.GetDashboardFromJsonIndex(file)
|
|
||||||
if dashboard == nil {
|
|
||||||
c.JsonApiErr(404, "Dashboard not found", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dash := dtos.DashboardFullWithMeta{Dashboard: dashboard.Data}
|
|
||||||
dash.Meta.Type = m.DashTypeJson
|
|
||||||
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
|
|
||||||
|
|
||||||
c.JSON(200, &dash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDashboardVersions returns all dashboard versions as JSON
|
// GetDashboardVersions returns all dashboard versions as JSON
|
||||||
func GetDashboardVersions(c *middleware.Context) Response {
|
func GetDashboardVersions(c *middleware.Context) Response {
|
||||||
dashboardId := c.ParamsInt64(":dashboardId")
|
dashId := c.ParamsInt64(":dashboardId")
|
||||||
limit := c.QueryInt("limit")
|
|
||||||
start := c.QueryInt("start")
|
|
||||||
|
|
||||||
if limit == 0 {
|
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||||
limit = 1000
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
query := m.GetDashboardVersionsQuery{
|
query := m.GetDashboardVersionsQuery{
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
DashboardId: dashboardId,
|
DashboardId: dashId,
|
||||||
Limit: limit,
|
Limit: c.QueryInt("limit"),
|
||||||
Start: start,
|
Start: c.QueryInt("start"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashboardId), err)
|
return ApiError(404, fmt.Sprintf("No versions found for dashboardId %d", dashId), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, version := range query.Result {
|
for _, version := range query.Result {
|
||||||
@ -300,17 +328,21 @@ func GetDashboardVersions(c *middleware.Context) Response {
|
|||||||
|
|
||||||
// GetDashboardVersion returns the dashboard version with the given ID.
|
// GetDashboardVersion returns the dashboard version with the given ID.
|
||||||
func GetDashboardVersion(c *middleware.Context) Response {
|
func GetDashboardVersion(c *middleware.Context) Response {
|
||||||
dashboardId := c.ParamsInt64(":dashboardId")
|
dashId := c.ParamsInt64(":dashboardId")
|
||||||
version := c.ParamsInt(":id")
|
|
||||||
|
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||||
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
query := m.GetDashboardVersionQuery{
|
query := m.GetDashboardVersionQuery{
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
DashboardId: dashboardId,
|
DashboardId: dashId,
|
||||||
Version: version,
|
Version: c.ParamsInt(":id"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", version, dashboardId), err)
|
return ApiError(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashId), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
creator := "Anonymous"
|
creator := "Anonymous"
|
||||||
@ -361,19 +393,21 @@ func CalculateDashboardDiff(c *middleware.Context, apiOptions dtos.CalculateDiff
|
|||||||
|
|
||||||
// RestoreDashboardVersion restores a dashboard to the given version.
|
// RestoreDashboardVersion restores a dashboard to the given version.
|
||||||
func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response {
|
func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboardVersionCommand) Response {
|
||||||
dashboardId := c.ParamsInt64(":dashboardId")
|
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"))
|
||||||
|
if rsp != nil {
|
||||||
dashQuery := m.GetDashboardQuery{Id: dashboardId, OrgId: c.OrgId}
|
return rsp
|
||||||
if err := bus.Dispatch(&dashQuery); err != nil {
|
|
||||||
return ApiError(404, "Dashboard not found", nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
versionQuery := m.GetDashboardVersionQuery{DashboardId: dashboardId, Version: apiCmd.Version, OrgId: c.OrgId}
|
guardian := guardian.NewDashboardGuardian(dash.Id, c.OrgId, c.SignedInUser)
|
||||||
|
if canSave, err := guardian.CanSave(); err != nil || !canSave {
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionQuery := m.GetDashboardVersionQuery{DashboardId: dash.Id, Version: apiCmd.Version, OrgId: c.OrgId}
|
||||||
if err := bus.Dispatch(&versionQuery); err != nil {
|
if err := bus.Dispatch(&versionQuery); err != nil {
|
||||||
return ApiError(404, "Dashboard version not found", nil)
|
return ApiError(404, "Dashboard version not found", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboard := dashQuery.Result
|
|
||||||
version := versionQuery.Result
|
version := versionQuery.Result
|
||||||
|
|
||||||
saveCmd := m.SaveDashboardCommand{}
|
saveCmd := m.SaveDashboardCommand{}
|
||||||
@ -381,7 +415,7 @@ func RestoreDashboardVersion(c *middleware.Context, apiCmd dtos.RestoreDashboard
|
|||||||
saveCmd.OrgId = c.OrgId
|
saveCmd.OrgId = c.OrgId
|
||||||
saveCmd.UserId = c.UserId
|
saveCmd.UserId = c.UserId
|
||||||
saveCmd.Dashboard = version.Data
|
saveCmd.Dashboard = version.Data
|
||||||
saveCmd.Dashboard.Set("version", dashboard.Version)
|
saveCmd.Dashboard.Set("version", dash.Version)
|
||||||
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
|
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
|
||||||
|
|
||||||
return PostDashboard(c, saveCmd)
|
return PostDashboard(c, saveCmd)
|
||||||
|
79
pkg/api/dashboard_acl.go
Normal file
79
pkg/api/dashboard_acl.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/guardian"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDashboardAclList(c *middleware.Context) Response {
|
||||||
|
dashId := c.ParamsInt64(":dashboardId")
|
||||||
|
|
||||||
|
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||||
|
|
||||||
|
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
acl, err := guardian.GetAcl()
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(500, "Failed to get dashboard acl", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, acl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateDashboardAcl(c *middleware.Context, apiCmd dtos.UpdateDashboardAclCommand) Response {
|
||||||
|
dashId := c.ParamsInt64(":dashboardId")
|
||||||
|
|
||||||
|
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||||
|
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := m.UpdateDashboardAclCommand{}
|
||||||
|
cmd.DashboardId = dashId
|
||||||
|
|
||||||
|
for _, item := range apiCmd.Items {
|
||||||
|
cmd.Items = append(cmd.Items, &m.DashboardAcl{
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
DashboardId: dashId,
|
||||||
|
UserId: item.UserId,
|
||||||
|
TeamId: item.TeamId,
|
||||||
|
Role: item.Role,
|
||||||
|
Permission: item.Permission,
|
||||||
|
Created: time.Now(),
|
||||||
|
Updated: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
|
if err == m.ErrDashboardAclInfoMissing || err == m.ErrDashboardPermissionDashboardEmpty {
|
||||||
|
return ApiError(409, err.Error(), err)
|
||||||
|
}
|
||||||
|
return ApiError(500, "Failed to create permission", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("Dashboard acl updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteDashboardAcl(c *middleware.Context) Response {
|
||||||
|
dashId := c.ParamsInt64(":dashboardId")
|
||||||
|
aclId := c.ParamsInt64(":aclId")
|
||||||
|
|
||||||
|
guardian := guardian.NewDashboardGuardian(dashId, c.OrgId, c.SignedInUser)
|
||||||
|
if canAdmin, err := guardian.CanAdmin(); err != nil || !canAdmin {
|
||||||
|
return dashboardGuardianResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := m.RemoveDashboardAclCommand{OrgId: c.OrgId, AclId: aclId}
|
||||||
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
|
return ApiError(500, "Failed to delete permission for user", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, "")
|
||||||
|
}
|
174
pkg/api/dashboard_acl_test.go
Normal file
174
pkg/api/dashboard_acl_test.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDashboardAclApiEndpoint(t *testing.T) {
|
||||||
|
Convey("Given a dashboard acl", t, func() {
|
||||||
|
mockResult := []*m.DashboardAclInfoDTO{
|
||||||
|
{Id: 1, OrgId: 1, DashboardId: 1, UserId: 2, Permission: m.PERMISSION_VIEW},
|
||||||
|
{Id: 2, OrgId: 1, DashboardId: 1, UserId: 3, Permission: m.PERMISSION_EDIT},
|
||||||
|
{Id: 3, OrgId: 1, DashboardId: 1, UserId: 4, Permission: m.PERMISSION_ADMIN},
|
||||||
|
{Id: 4, OrgId: 1, DashboardId: 1, TeamId: 1, Permission: m.PERMISSION_VIEW},
|
||||||
|
{Id: 5, OrgId: 1, DashboardId: 1, TeamId: 2, Permission: m.PERMISSION_ADMIN},
|
||||||
|
}
|
||||||
|
dtoRes := transformDashboardAclsToDTOs(mockResult)
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = dtoRes
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = mockResult
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
teamResp := []*m.Team{}
|
||||||
|
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||||
|
query.Result = teamResp
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is org admin", func() {
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_ADMIN, func(sc *scenarioContext) {
|
||||||
|
Convey("Should be able to access ACL", func() {
|
||||||
|
sc.handlerFunc = GetDashboardAclList
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
|
||||||
|
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(len(respJSON.MustArray()), ShouldEqual, 5)
|
||||||
|
So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2)
|
||||||
|
So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, m.PERMISSION_VIEW)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is editor and has admin permission in the ACL", func() {
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||||
|
|
||||||
|
Convey("Should be able to access ACL", func() {
|
||||||
|
sc.handlerFunc = GetDashboardAclList
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_ADMIN})
|
||||||
|
|
||||||
|
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should be able to delete permission", func() {
|
||||||
|
sc.handlerFunc = DeleteDashboardAcl
|
||||||
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is a member of a team in the ACL with admin permission", func() {
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardsId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
teamResp = append(teamResp, &m.Team{Id: 2, OrgId: 1, Name: "UG2"})
|
||||||
|
|
||||||
|
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should be able to delete permission", func() {
|
||||||
|
sc.handlerFunc = DeleteDashboardAcl
|
||||||
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is editor and has edit permission in the ACL", func() {
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
|
||||||
|
|
||||||
|
Convey("Should not be able to access ACL", func() {
|
||||||
|
sc.handlerFunc = GetDashboardAclList
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/1", "/api/dashboards/id/:dashboardId/acl/:aclId", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_EDIT})
|
||||||
|
|
||||||
|
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should be not be able to delete permission", func() {
|
||||||
|
sc.handlerFunc = DeleteDashboardAcl
|
||||||
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is editor and not in the ACL", func() {
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/acl", "/api/dashboards/id/:dashboardsId/acl", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
|
||||||
|
Convey("Should not be able to access ACL", func() {
|
||||||
|
sc.handlerFunc = GetDashboardAclList
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/id/1/acl/user/1", "/api/dashboards/id/:dashboardsId/acl/user/:userId", m.ROLE_EDITOR, func(sc *scenarioContext) {
|
||||||
|
mockResult = append(mockResult, &m.DashboardAclInfoDTO{Id: 1, OrgId: 1, DashboardId: 1, UserId: 1, Permission: m.PERMISSION_VIEW})
|
||||||
|
bus.AddHandler("test3", func(cmd *m.RemoveDashboardAclCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should be not be able to delete permission", func() {
|
||||||
|
sc.handlerFunc = DeleteDashboardAcl
|
||||||
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformDashboardAclsToDTOs(acls []*m.DashboardAclInfoDTO) []*m.DashboardAclInfoDTO {
|
||||||
|
dtos := make([]*m.DashboardAclInfoDTO, 0)
|
||||||
|
|
||||||
|
for _, acl := range acls {
|
||||||
|
dto := &m.DashboardAclInfoDTO{
|
||||||
|
Id: acl.Id,
|
||||||
|
OrgId: acl.OrgId,
|
||||||
|
DashboardId: acl.DashboardId,
|
||||||
|
Permission: acl.Permission,
|
||||||
|
UserId: acl.UserId,
|
||||||
|
TeamId: acl.TeamId,
|
||||||
|
}
|
||||||
|
dtos = append(dtos, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dtos
|
||||||
|
}
|
521
pkg/api/dashboard_test.go
Normal file
521
pkg/api/dashboard_test.go
Normal file
@ -0,0 +1,521 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
macaron "gopkg.in/macaron.v1"
|
||||||
|
|
||||||
|
"github.com/go-macaron/session"
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeDashboardRepo struct {
|
||||||
|
inserted []*dashboards.SaveDashboardItem
|
||||||
|
getDashboard []*m.Dashboard
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *fakeDashboardRepo) SaveDashboard(json *dashboards.SaveDashboardItem) (*m.Dashboard, error) {
|
||||||
|
repo.inserted = append(repo.inserted, json)
|
||||||
|
return json.Dashboard, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var fakeRepo *fakeDashboardRepo
|
||||||
|
|
||||||
|
func TestDashboardApiEndpoint(t *testing.T) {
|
||||||
|
Convey("Given a dashboard with a parent folder which does not have an acl", t, func() {
|
||||||
|
fakeDash := m.NewDashboard("Child dash")
|
||||||
|
fakeDash.Id = 1
|
||||||
|
fakeDash.FolderId = 1
|
||||||
|
fakeDash.HasAcl = false
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||||
|
query.Result = fakeDash
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
viewerRole := m.ROLE_VIEWER
|
||||||
|
editorRole := m.ROLE_EDITOR
|
||||||
|
|
||||||
|
aclMockResp := []*m.DashboardAclInfoDTO{
|
||||||
|
{Role: &viewerRole, Permission: m.PERMISSION_VIEW},
|
||||||
|
{Role: &editorRole, Permission: m.PERMISSION_EDIT},
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = aclMockResp
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||||
|
query.Result = []*m.Team{}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := m.SaveDashboardCommand{
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"folderId": fakeDash.FolderId,
|
||||||
|
"title": fakeDash.Title,
|
||||||
|
"id": fakeDash.Id,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("When user is an Org Viewer", func() {
|
||||||
|
role := m.ROLE_VIEWER
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
dash := GetDashboardShouldReturn200(sc)
|
||||||
|
|
||||||
|
Convey("Should not be able to edit or save dashboard", func() {
|
||||||
|
So(dash.Meta.CanEdit, ShouldBeFalse)
|
||||||
|
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||||
|
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersion(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||||
|
CallPostDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is an Org Editor", func() {
|
||||||
|
role := m.ROLE_EDITOR
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
dash := GetDashboardShouldReturn200(sc)
|
||||||
|
|
||||||
|
Convey("Should be able to edit or save dashboard", func() {
|
||||||
|
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||||
|
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||||
|
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersion(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||||
|
CallPostDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When saving a dashboard folder in another folder", func() {
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||||
|
query.Result = fakeDash
|
||||||
|
query.Result.IsFolder = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
invalidCmd := m.SaveDashboardCommand{
|
||||||
|
FolderId: fakeDash.FolderId,
|
||||||
|
IsFolder: true,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"folderId": fakeDash.FolderId,
|
||||||
|
"title": fakeDash.Title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
Convey("Should return an error", func() {
|
||||||
|
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, invalidCmd, func(sc *scenarioContext) {
|
||||||
|
CallPostDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given a dashboard with a parent folder which has an acl", t, func() {
|
||||||
|
fakeDash := m.NewDashboard("Child dash")
|
||||||
|
fakeDash.Id = 1
|
||||||
|
fakeDash.FolderId = 1
|
||||||
|
fakeDash.HasAcl = true
|
||||||
|
setting.ViewersCanEdit = false
|
||||||
|
|
||||||
|
aclMockResp := []*m.DashboardAclInfoDTO{
|
||||||
|
{
|
||||||
|
DashboardId: 1,
|
||||||
|
Permission: m.PERMISSION_EDIT,
|
||||||
|
UserId: 200,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = aclMockResp
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
|
||||||
|
query.Result = fakeDash
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error {
|
||||||
|
query.Result = []*m.Team{}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := m.SaveDashboardCommand{
|
||||||
|
FolderId: fakeDash.FolderId,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"id": fakeDash.Id,
|
||||||
|
"folderId": fakeDash.FolderId,
|
||||||
|
"title": fakeDash.Title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("When user is an Org Viewer and has no permissions for this dashboard", func() {
|
||||||
|
role := m.ROLE_VIEWER
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
sc.handlerFunc = GetDashboard
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
Convey("Should be denied access", func() {
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersion(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||||
|
CallPostDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is an Org Editor and has no permissions for this dashboard", func() {
|
||||||
|
role := m.ROLE_EDITOR
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
sc.handlerFunc = GetDashboard
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
Convey("Should be denied access", func() {
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersion(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||||
|
CallPostDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is an Org Viewer but has an edit permission", func() {
|
||||||
|
role := m.ROLE_VIEWER
|
||||||
|
|
||||||
|
mockResult := []*m.DashboardAclInfoDTO{
|
||||||
|
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_EDIT},
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = mockResult
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
dash := GetDashboardShouldReturn200(sc)
|
||||||
|
|
||||||
|
Convey("Should be able to get dashboard with edit rights", func() {
|
||||||
|
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||||
|
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||||
|
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersion(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||||
|
CallPostDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is an Org Viewer and viewers can edit", func() {
|
||||||
|
role := m.ROLE_VIEWER
|
||||||
|
setting.ViewersCanEdit = true
|
||||||
|
|
||||||
|
mockResult := []*m.DashboardAclInfoDTO{
|
||||||
|
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = mockResult
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
dash := GetDashboardShouldReturn200(sc)
|
||||||
|
|
||||||
|
Convey("Should be able to get dashboard with edit rights but can save should be false", func() {
|
||||||
|
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||||
|
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||||
|
So(dash.Meta.CanAdmin, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is an Org Viewer but has an admin permission", func() {
|
||||||
|
role := m.ROLE_VIEWER
|
||||||
|
|
||||||
|
mockResult := []*m.DashboardAclInfoDTO{
|
||||||
|
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_ADMIN},
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = mockResult
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
dash := GetDashboardShouldReturn200(sc)
|
||||||
|
|
||||||
|
Convey("Should be able to get dashboard with edit rights", func() {
|
||||||
|
So(dash.Meta.CanEdit, ShouldBeTrue)
|
||||||
|
So(dash.Meta.CanSave, ShouldBeTrue)
|
||||||
|
So(dash.Meta.CanAdmin, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersion(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||||
|
CallPostDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When user is an Org Editor but has a view permission", func() {
|
||||||
|
role := m.ROLE_EDITOR
|
||||||
|
|
||||||
|
mockResult := []*m.DashboardAclInfoDTO{
|
||||||
|
{Id: 1, OrgId: 1, DashboardId: 2, UserId: 1, Permission: m.PERMISSION_VIEW},
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardAclInfoListQuery) error {
|
||||||
|
query.Result = mockResult
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
dash := GetDashboardShouldReturn200(sc)
|
||||||
|
|
||||||
|
Convey("Should not be able to edit or save dashboard", func() {
|
||||||
|
So(dash.Meta.CanEdit, ShouldBeFalse)
|
||||||
|
So(dash.Meta.CanSave, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/2", "/api/dashboards/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallDeleteDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersion(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) {
|
||||||
|
CallGetDashboardVersions(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", role, cmd, func(sc *scenarioContext) {
|
||||||
|
CallPostDashboard(sc)
|
||||||
|
So(sc.resp.Code, ShouldEqual, 403)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
|
||||||
|
sc.handlerFunc = GetDashboard
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sc.resp.Code, ShouldEqual, 200)
|
||||||
|
|
||||||
|
dash := dtos.DashboardFullWithMeta{}
|
||||||
|
err := json.NewDecoder(sc.resp.Body).Decode(&dash)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
return dash
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallGetDashboardVersion(sc *scenarioContext) {
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
|
||||||
|
query.Result = &m.DashboardVersion{}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.handlerFunc = GetDashboardVersion
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallGetDashboardVersions(sc *scenarioContext) {
|
||||||
|
bus.AddHandler("test", func(query *m.GetDashboardVersionsQuery) error {
|
||||||
|
query.Result = []*m.DashboardVersionDTO{}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.handlerFunc = GetDashboardVersions
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallDeleteDashboard(sc *scenarioContext) {
|
||||||
|
bus.AddHandler("test", func(cmd *m.DeleteDashboardCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.handlerFunc = DeleteDashboard
|
||||||
|
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallPostDashboard(sc *scenarioContext) {
|
||||||
|
bus.AddHandler("test", func(cmd *alerting.ValidateDashboardAlertsCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
|
||||||
|
cmd.Result = &m.Dashboard{Id: 2, Slug: "Dash", Version: 2}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
bus.AddHandler("test", func(cmd *alerting.UpdateDashboardAlertsCommand) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func postDashboardScenario(desc string, url string, routePattern string, role m.RoleType, cmd m.SaveDashboardCommand, fn scenarioFunc) {
|
||||||
|
Convey(desc+" "+url, func() {
|
||||||
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
|
sc := &scenarioContext{
|
||||||
|
url: url,
|
||||||
|
}
|
||||||
|
viewsPath, _ := filepath.Abs("../../public/views")
|
||||||
|
|
||||||
|
sc.m = macaron.New()
|
||||||
|
sc.m.Use(macaron.Renderer(macaron.RenderOptions{
|
||||||
|
Directory: viewsPath,
|
||||||
|
Delims: macaron.Delims{Left: "[[", Right: "]]"},
|
||||||
|
}))
|
||||||
|
|
||||||
|
sc.m.Use(middleware.GetContextHandler())
|
||||||
|
sc.m.Use(middleware.Sessioner(&session.Options{}))
|
||||||
|
|
||||||
|
sc.defaultHandler = wrap(func(c *middleware.Context) Response {
|
||||||
|
sc.context = c
|
||||||
|
sc.context.UserId = TestUserID
|
||||||
|
sc.context.OrgId = TestOrgID
|
||||||
|
sc.context.OrgRole = role
|
||||||
|
|
||||||
|
return PostDashboard(c, cmd)
|
||||||
|
})
|
||||||
|
|
||||||
|
fakeRepo = &fakeDashboardRepo{}
|
||||||
|
dashboards.SetRepository(fakeRepo)
|
||||||
|
|
||||||
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
@ -56,6 +56,10 @@ func TestDataSourcesProxy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
||||||
|
loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) {
|
||||||
Convey(desc+" "+url, func() {
|
Convey(desc+" "+url, func() {
|
||||||
defer bus.ClearBusHandlers()
|
defer bus.ClearBusHandlers()
|
||||||
|
|
||||||
@ -77,7 +81,7 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
|||||||
sc.context = c
|
sc.context = c
|
||||||
sc.context.UserId = TestUserID
|
sc.context.UserId = TestUserID
|
||||||
sc.context.OrgId = TestOrgID
|
sc.context.OrgId = TestOrgID
|
||||||
sc.context.OrgRole = models.ROLE_EDITOR
|
sc.context.OrgRole = role
|
||||||
if sc.handlerFunc != nil {
|
if sc.handlerFunc != nil {
|
||||||
return sc.handlerFunc(sc.context)
|
return sc.handlerFunc(sc.context)
|
||||||
}
|
}
|
||||||
@ -85,7 +89,12 @@ func loggedInUserScenario(desc string, url string, fn scenarioFunc) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
sc.m.Get(url, sc.defaultHandler)
|
switch method {
|
||||||
|
case "GET":
|
||||||
|
sc.m.Get(routePattern, sc.defaultHandler)
|
||||||
|
case "DELETE":
|
||||||
|
sc.m.Delete(routePattern, sc.defaultHandler)
|
||||||
|
}
|
||||||
|
|
||||||
fn(sc)
|
fn(sc)
|
||||||
})
|
})
|
||||||
|
16
pkg/api/dtos/acl.go
Normal file
16
pkg/api/dtos/acl.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package dtos
|
||||||
|
|
||||||
|
import (
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateDashboardAclCommand struct {
|
||||||
|
Items []DashboardAclUpdateItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardAclUpdateItem struct {
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
TeamId int64 `json:"teamId"`
|
||||||
|
Role *m.RoleType `json:"role,omitempty"`
|
||||||
|
Permission m.PermissionType `json:"permission"`
|
||||||
|
}
|
@ -13,6 +13,7 @@ type DashboardMeta struct {
|
|||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
CanSave bool `json:"canSave"`
|
CanSave bool `json:"canSave"`
|
||||||
CanEdit bool `json:"canEdit"`
|
CanEdit bool `json:"canEdit"`
|
||||||
|
CanAdmin bool `json:"canAdmin"`
|
||||||
CanStar bool `json:"canStar"`
|
CanStar bool `json:"canStar"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Expires time.Time `json:"expires"`
|
Expires time.Time `json:"expires"`
|
||||||
@ -21,6 +22,10 @@ type DashboardMeta struct {
|
|||||||
UpdatedBy string `json:"updatedBy"`
|
UpdatedBy string `json:"updatedBy"`
|
||||||
CreatedBy string `json:"createdBy"`
|
CreatedBy string `json:"createdBy"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
|
HasAcl bool `json:"hasAcl"`
|
||||||
|
IsFolder bool `json:"isFolder"`
|
||||||
|
FolderId int64 `json:"folderId"`
|
||||||
|
FolderTitle string `json:"folderTitle"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardFullWithMeta struct {
|
type DashboardFullWithMeta struct {
|
||||||
|
@ -7,9 +7,10 @@ type IndexViewData struct {
|
|||||||
AppSubUrl string
|
AppSubUrl string
|
||||||
GoogleAnalyticsId string
|
GoogleAnalyticsId string
|
||||||
GoogleTagManagerId string
|
GoogleTagManagerId string
|
||||||
MainNavLinks []*NavLink
|
NavTree []*NavLink
|
||||||
BuildVersion string
|
BuildVersion string
|
||||||
BuildCommit string
|
BuildCommit string
|
||||||
|
Theme string
|
||||||
NewGrafanaVersionExists bool
|
NewGrafanaVersionExists bool
|
||||||
NewGrafanaVersion string
|
NewGrafanaVersion string
|
||||||
}
|
}
|
||||||
@ -20,10 +21,16 @@ type PluginCss struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NavLink struct {
|
type NavLink struct {
|
||||||
|
Id string `json:"id,omitempty"`
|
||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
SubTitle string `json:"subTitle,omitempty"`
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
Img string `json:"img,omitempty"`
|
Img string `json:"img,omitempty"`
|
||||||
Url string `json:"url,omitempty"`
|
Url string `json:"url,omitempty"`
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
Divider bool `json:"divider,omitempty"`
|
Divider bool `json:"divider,omitempty"`
|
||||||
|
HideFromMenu bool `json:"hideFromMenu,omitempty"`
|
||||||
|
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
||||||
Children []*NavLink `json:"children,omitempty"`
|
Children []*NavLink `json:"children,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ type AddInviteForm struct {
|
|||||||
LoginOrEmail string `json:"loginOrEmail" binding:"Required"`
|
LoginOrEmail string `json:"loginOrEmail" binding:"Required"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Role m.RoleType `json:"role" binding:"Required"`
|
Role m.RoleType `json:"role" binding:"Required"`
|
||||||
SkipEmails bool `json:"skipEmails"`
|
SendEmail bool `json:"sendEmail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InviteInfo struct {
|
type InviteInfo struct {
|
||||||
|
@ -3,6 +3,7 @@ package dtos
|
|||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
@ -27,6 +28,7 @@ type CurrentUser struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
LightTheme bool `json:"lightTheme"`
|
LightTheme bool `json:"lightTheme"`
|
||||||
|
OrgCount int `json:"orgCount"`
|
||||||
OrgId int64 `json:"orgId"`
|
OrgId int64 `json:"orgId"`
|
||||||
OrgName string `json:"orgName"`
|
OrgName string `json:"orgName"`
|
||||||
OrgRole m.RoleType `json:"orgRole"`
|
OrgRole m.RoleType `json:"orgRole"`
|
||||||
@ -56,3 +58,19 @@ func GetGravatarUrl(text string) string {
|
|||||||
hasher.Write([]byte(strings.ToLower(text)))
|
hasher.Write([]byte(strings.ToLower(text)))
|
||||||
return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil))
|
return fmt.Sprintf(setting.AppSubUrl+"/avatar/%x", hasher.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetGravatarUrlWithDefault(text string, defaultText string) string {
|
||||||
|
if text != "" {
|
||||||
|
return GetGravatarUrl(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
reg, err := regexp.Compile("[^a-zA-Z0-9]+")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
text = reg.ReplaceAllString(defaultText, "") + "@localhost"
|
||||||
|
|
||||||
|
return GetGravatarUrl(text)
|
||||||
|
}
|
||||||
|
@ -143,7 +143,6 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
|||||||
"alertingEnabled": setting.AlertingEnabled,
|
"alertingEnabled": setting.AlertingEnabled,
|
||||||
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
||||||
"disableLoginForm": setting.DisableLoginForm,
|
"disableLoginForm": setting.DisableLoginForm,
|
||||||
"disableSignoutMenu": setting.DisableSignoutMenu,
|
|
||||||
"externalUserMngInfo": setting.ExternalUserMngInfo,
|
"externalUserMngInfo": setting.ExternalUserMngInfo,
|
||||||
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
"externalUserMngLinkUrl": setting.ExternalUserMngLinkUrl,
|
||||||
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
"externalUserMngLinkName": setting.ExternalUserMngLinkName,
|
||||||
|
@ -95,7 +95,7 @@ func (hs *HttpServer) Start(ctx context.Context) error {
|
|||||||
|
|
||||||
func (hs *HttpServer) Shutdown(ctx context.Context) error {
|
func (hs *HttpServer) Shutdown(ctx context.Context) error {
|
||||||
err := hs.httpSrv.Shutdown(ctx)
|
err := hs.httpSrv.Shutdown(ctx)
|
||||||
hs.log.Info("stopped http server")
|
hs.log.Info("Stopped HTTP server")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
185
pkg/api/index.go
185
pkg/api/index.go
@ -50,6 +50,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
Login: c.Login,
|
Login: c.Login,
|
||||||
Email: c.Email,
|
Email: c.Email,
|
||||||
Name: c.Name,
|
Name: c.Name,
|
||||||
|
OrgCount: c.OrgCount,
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
OrgName: c.OrgName,
|
OrgName: c.OrgName,
|
||||||
OrgRole: c.OrgRole,
|
OrgRole: c.OrgRole,
|
||||||
@ -61,6 +62,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
HelpFlags1: c.HelpFlags1,
|
HelpFlags1: c.HelpFlags1,
|
||||||
},
|
},
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
|
Theme: prefs.Theme,
|
||||||
AppUrl: appUrl,
|
AppUrl: appUrl,
|
||||||
AppSubUrl: appSubUrl,
|
AppSubUrl: appSubUrl,
|
||||||
GoogleAnalyticsId: setting.GoogleAnalyticsId,
|
GoogleAnalyticsId: setting.GoogleAnalyticsId,
|
||||||
@ -82,55 +84,80 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
themeUrlParam := c.Query("theme")
|
themeUrlParam := c.Query("theme")
|
||||||
if themeUrlParam == "light" {
|
if themeUrlParam == "light" {
|
||||||
data.User.LightTheme = true
|
data.User.LightTheme = true
|
||||||
}
|
data.Theme = "light"
|
||||||
|
|
||||||
dashboardChildNavs := []*dtos.NavLink{
|
|
||||||
{Text: "Home", Url: setting.AppSubUrl + "/"},
|
|
||||||
{Text: "Playlists", Url: setting.AppSubUrl + "/playlists"},
|
|
||||||
{Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true})
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"})
|
Text: "Create",
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"})
|
Id: "create",
|
||||||
|
Icon: "fa fa-fw fa-plus",
|
||||||
|
Url: setting.AppSubUrl + "/dashboard/new",
|
||||||
|
Children: []*dtos.NavLink{
|
||||||
|
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
|
||||||
|
{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"},
|
||||||
|
{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"},
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
dashboardChildNavs := []*dtos.NavLink{
|
||||||
|
{Text: "Home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
|
||||||
|
{Divider: true, HideFromTabs: true},
|
||||||
|
{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"},
|
||||||
|
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"},
|
||||||
|
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"},
|
||||||
|
}
|
||||||
|
|
||||||
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||||
Text: "Dashboards",
|
Text: "Dashboards",
|
||||||
Icon: "icon-gf icon-gf-dashboard",
|
Id: "dashboards",
|
||||||
|
SubTitle: "Manage dashboards & folders",
|
||||||
|
Icon: "gicon gicon-dashboard",
|
||||||
Url: setting.AppSubUrl + "/",
|
Url: setting.AppSubUrl + "/",
|
||||||
Children: dashboardChildNavs,
|
Children: dashboardChildNavs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if c.IsSignedIn {
|
||||||
|
profileNode := &dtos.NavLink{
|
||||||
|
Text: c.SignedInUser.NameOrFallback(),
|
||||||
|
SubTitle: c.SignedInUser.Login,
|
||||||
|
Id: "profile",
|
||||||
|
Img: data.User.GravatarUrl,
|
||||||
|
Url: setting.AppSubUrl + "/profile",
|
||||||
|
HideFromMenu: true,
|
||||||
|
Children: []*dtos.NavLink{
|
||||||
|
{Text: "Preferences", Id: "profile-settings", Url: setting.AppSubUrl + "/profile", Icon: "gicon gicon-preferences"},
|
||||||
|
{Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !setting.DisableSignoutMenu {
|
||||||
|
// add sign out first
|
||||||
|
profileNode.Children = append(profileNode.Children, &dtos.NavLink{
|
||||||
|
Text: "Sign out", Id: "sign-out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data.NavTree = append(data.NavTree, profileNode)
|
||||||
|
}
|
||||||
|
|
||||||
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
||||||
alertChildNavs := []*dtos.NavLink{
|
alertChildNavs := []*dtos.NavLink{
|
||||||
{Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"},
|
{Text: "Alert Rules", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "gicon gicon-alert-rules"},
|
||||||
{Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"},
|
{Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "gicon gicon-alert-notification-channel"},
|
||||||
}
|
}
|
||||||
|
|
||||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||||
Text: "Alerting",
|
Text: "Alerting",
|
||||||
Icon: "icon-gf icon-gf-alert",
|
SubTitle: "Alert rules & notifications",
|
||||||
|
Id: "alerting",
|
||||||
|
Icon: "gicon gicon-alert",
|
||||||
Url: setting.AppSubUrl + "/alerting/list",
|
Url: setting.AppSubUrl + "/alerting/list",
|
||||||
Children: alertChildNavs,
|
Children: alertChildNavs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.OrgRole == m.ROLE_ADMIN {
|
|
||||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
|
||||||
Text: "Data Sources",
|
|
||||||
Icon: "icon-gf icon-gf-datasources",
|
|
||||||
Url: setting.AppSubUrl + "/datasources",
|
|
||||||
})
|
|
||||||
|
|
||||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
|
||||||
Text: "Plugins",
|
|
||||||
Icon: "icon-gf icon-gf-apps",
|
|
||||||
Url: setting.AppSubUrl + "/plugins",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
|
enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -140,6 +167,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
if plugin.Pinned {
|
if plugin.Pinned {
|
||||||
appLink := &dtos.NavLink{
|
appLink := &dtos.NavLink{
|
||||||
Text: plugin.Name,
|
Text: plugin.Name,
|
||||||
|
Id: "plugin-page-" + plugin.Id,
|
||||||
Url: plugin.DefaultNavUrl,
|
Url: plugin.DefaultNavUrl,
|
||||||
Img: plugin.Info.Logos.Small,
|
Img: plugin.Info.Logos.Small,
|
||||||
}
|
}
|
||||||
@ -168,29 +196,106 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
|
|
||||||
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
|
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
|
||||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
|
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
|
||||||
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "fa fa-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/edit"})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(appLink.Children) > 0 {
|
if len(appLink.Children) > 0 {
|
||||||
data.MainNavLinks = append(data.MainNavLinks, appLink)
|
data.NavTree = append(data.NavTree, appLink)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.IsGrafanaAdmin {
|
if c.OrgRole == m.ROLE_ADMIN {
|
||||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
cfgNode := &dtos.NavLink{
|
||||||
Text: "Admin",
|
Id: "cfg",
|
||||||
Icon: "fa fa-fw fa-cogs",
|
Text: "Configuration",
|
||||||
Url: setting.AppSubUrl + "/admin",
|
SubTitle: "Organization: " + c.OrgName,
|
||||||
|
Icon: "gicon gicon-cog",
|
||||||
|
Url: setting.AppSubUrl + "/datasources",
|
||||||
Children: []*dtos.NavLink{
|
Children: []*dtos.NavLink{
|
||||||
{Text: "Global Users", Url: setting.AppSubUrl + "/admin/users"},
|
{
|
||||||
{Text: "Global Orgs", Url: setting.AppSubUrl + "/admin/orgs"},
|
Text: "Data Sources",
|
||||||
{Text: "Server Settings", Url: setting.AppSubUrl + "/admin/settings"},
|
Icon: "gicon gicon-datasources",
|
||||||
{Text: "Server Stats", Url: setting.AppSubUrl + "/admin/stats"},
|
Description: "Add and configure data sources",
|
||||||
|
Id: "datasources",
|
||||||
|
Url: setting.AppSubUrl + "/datasources",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "Users",
|
||||||
|
Id: "users",
|
||||||
|
Description: "Manage org members",
|
||||||
|
Icon: "gicon gicon-user",
|
||||||
|
Url: setting.AppSubUrl + "/org/users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "Teams",
|
||||||
|
Id: "teams",
|
||||||
|
Description: "Manage org groups",
|
||||||
|
Icon: "gicon gicon-team",
|
||||||
|
Url: setting.AppSubUrl + "/org/teams",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "Plugins",
|
||||||
|
Id: "plugins",
|
||||||
|
Description: "View and configure plugins",
|
||||||
|
Icon: "gicon gicon-plugins",
|
||||||
|
Url: setting.AppSubUrl + "/plugins",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "Preferences",
|
||||||
|
Id: "org-settings",
|
||||||
|
Description: "Organization preferences",
|
||||||
|
Icon: "gicon gicon-preferences",
|
||||||
|
Url: setting.AppSubUrl + "/org",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Text: "API Keys",
|
||||||
|
Id: "apikeys",
|
||||||
|
Description: "Create & manage API keys",
|
||||||
|
Icon: "gicon gicon-apikeys",
|
||||||
|
Url: setting.AppSubUrl + "/org/apikeys",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsGrafanaAdmin {
|
||||||
|
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||||
|
Divider: true, HideFromTabs: true,
|
||||||
|
})
|
||||||
|
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||||
|
Text: "Server Admin",
|
||||||
|
HideFromTabs: true,
|
||||||
|
SubTitle: "Manage all users & orgs",
|
||||||
|
Id: "admin",
|
||||||
|
Icon: "gicon gicon-shield",
|
||||||
|
Url: setting.AppSubUrl + "/admin/users",
|
||||||
|
Children: []*dtos.NavLink{
|
||||||
|
{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
|
||||||
|
{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
|
||||||
|
{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
|
||||||
|
{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
|
||||||
|
{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.NavTree = append(data.NavTree, cfgNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||||
|
Text: "Help",
|
||||||
|
Id: "help",
|
||||||
|
Url: "#",
|
||||||
|
Icon: "gicon gicon-question",
|
||||||
|
HideFromMenu: true,
|
||||||
|
Children: []*dtos.NavLink{
|
||||||
|
{Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"},
|
||||||
|
{Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"},
|
||||||
|
{Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
// send invite email
|
// send invite email
|
||||||
if !inviteDto.SkipEmails && util.IsEmail(inviteDto.LoginOrEmail) {
|
if inviteDto.SendEmail && util.IsEmail(inviteDto.LoginOrEmail) {
|
||||||
emailCmd := m.SendEmailCommand{
|
emailCmd := m.SendEmailCommand{
|
||||||
To: []string{inviteDto.LoginOrEmail},
|
To: []string{inviteDto.LoginOrEmail},
|
||||||
Template: "new_user_invite.html",
|
Template: "new_user_invite.html",
|
||||||
@ -99,7 +99,7 @@ func inviteExistingUserToOrg(c *middleware.Context, user *m.User, inviteDto *dto
|
|||||||
return ApiError(500, "Error while trying to create org user", err)
|
return ApiError(500, "Error while trying to create org user", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if !inviteDto.SkipEmails && util.IsEmail(user.Email) {
|
if inviteDto.SendEmail && util.IsEmail(user.Email) {
|
||||||
emailCmd := m.SendEmailCommand{
|
emailCmd := m.SendEmailCommand{
|
||||||
To: []string{user.Email},
|
To: []string{user.Email},
|
||||||
Template: "invited_to_org.html",
|
Template: "invited_to_org.html",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
@ -31,10 +32,6 @@ func addOrgUserHelper(cmd m.AddOrgUserCommand) Response {
|
|||||||
|
|
||||||
userToAdd := userQuery.Result
|
userToAdd := userQuery.Result
|
||||||
|
|
||||||
// if userToAdd.Id == c.UserId {
|
|
||||||
// return ApiError(400, "Cannot add yourself as user", nil)
|
|
||||||
// }
|
|
||||||
|
|
||||||
cmd.UserId = userToAdd.Id
|
cmd.UserId = userToAdd.Id
|
||||||
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
@ -64,6 +61,10 @@ func getOrgUsersHelper(orgId int64) Response {
|
|||||||
return ApiError(500, "Failed to get account user", err)
|
return ApiError(500, "Failed to get account user", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, user := range query.Result {
|
||||||
|
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||||
|
}
|
||||||
|
|
||||||
return Json(200, query.Result)
|
return Json(200, query.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ func GetPlaylistItems(c *middleware.Context) Response {
|
|||||||
func GetPlaylistDashboards(c *middleware.Context) Response {
|
func GetPlaylistDashboards(c *middleware.Context) Response {
|
||||||
playlistId := c.ParamsInt64(":id")
|
playlistId := c.ParamsInt64(":id")
|
||||||
|
|
||||||
playlists, err := LoadPlaylistDashboards(c.OrgId, c.UserId, playlistId)
|
playlists, err := LoadPlaylistDashboards(c.OrgId, c.SignedInUser, playlistId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ApiError(500, "Could not load dashboards", err)
|
return ApiError(500, "Could not load dashboards", err)
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ func populateDashboardsById(dashboardByIds []int64, dashboardIdOrder map[int64]i
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
|
func populateDashboardsByTag(orgId int64, signedInUser *m.SignedInUser, dashboardByTag []string, dashboardTagOrder map[string]int) dtos.PlaylistDashboardsSlice {
|
||||||
result := make(dtos.PlaylistDashboardsSlice, 0)
|
result := make(dtos.PlaylistDashboardsSlice, 0)
|
||||||
|
|
||||||
if len(dashboardByTag) > 0 {
|
if len(dashboardByTag) > 0 {
|
||||||
@ -42,7 +42,7 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb
|
|||||||
searchQuery := search.Query{
|
searchQuery := search.Query{
|
||||||
Title: "",
|
Title: "",
|
||||||
Tags: []string{tag},
|
Tags: []string{tag},
|
||||||
UserId: userId,
|
SignedInUser: signedInUser,
|
||||||
Limit: 100,
|
Limit: 100,
|
||||||
IsStarred: false,
|
IsStarred: false,
|
||||||
OrgId: orgId,
|
OrgId: orgId,
|
||||||
@ -64,7 +64,7 @@ func populateDashboardsByTag(orgId, userId int64, dashboardByTag []string, dashb
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
|
func LoadPlaylistDashboards(orgId int64, signedInUser *m.SignedInUser, playlistId int64) (dtos.PlaylistDashboardsSlice, error) {
|
||||||
playlistItems, _ := LoadPlaylistItems(playlistId)
|
playlistItems, _ := LoadPlaylistItems(playlistId)
|
||||||
|
|
||||||
dashboardByIds := make([]int64, 0)
|
dashboardByIds := make([]int64, 0)
|
||||||
@ -89,7 +89,7 @@ func LoadPlaylistDashboards(orgId, userId, playlistId int64) (dtos.PlaylistDashb
|
|||||||
|
|
||||||
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
|
var k, _ = populateDashboardsById(dashboardByIds, dashboardIdOrder)
|
||||||
result = append(result, k...)
|
result = append(result, k...)
|
||||||
result = append(result, populateDashboardsByTag(orgId, userId, dashboardByTag, dashboardTagOrder)...)
|
result = append(result, populateDashboardsByTag(orgId, signedInUser, dashboardByTag, dashboardTagOrder)...)
|
||||||
|
|
||||||
sort.Sort(result)
|
sort.Sort(result)
|
||||||
return result, nil
|
return result, nil
|
||||||
|
@ -135,9 +135,24 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) {
|
|||||||
req.Header.Add("Authorization", dsAuth)
|
req.Header.Add("Authorization", dsAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear cookie headers
|
// clear cookie header, except for whitelisted cookies
|
||||||
|
var keptCookies []*http.Cookie
|
||||||
|
if proxy.ds.JsonData != nil {
|
||||||
|
if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil {
|
||||||
|
keepCookieNames := keepCookies.MustStringArray()
|
||||||
|
for _, c := range req.Cookies() {
|
||||||
|
for _, v := range keepCookieNames {
|
||||||
|
if c.Name == v {
|
||||||
|
keptCookies = append(keptCookies, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
req.Header.Del("Cookie")
|
req.Header.Del("Cookie")
|
||||||
req.Header.Del("Set-Cookie")
|
for _, c := range keptCookies {
|
||||||
|
req.AddCookie(c)
|
||||||
|
}
|
||||||
|
|
||||||
// clear X-Forwarded Host/Port/Proto headers
|
// clear X-Forwarded Host/Port/Proto headers
|
||||||
req.Header.Del("X-Forwarded-Host")
|
req.Header.Del("X-Forwarded-Host")
|
||||||
|
@ -149,6 +149,58 @@ func TestDSRouteRule(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("When proxying a data source with no keepCookies specified", func() {
|
||||||
|
plugin := &plugins.DataSourcePlugin{}
|
||||||
|
|
||||||
|
json, _ := simplejson.NewJson([]byte(`{"keepCookies": []}`))
|
||||||
|
|
||||||
|
ds := &m.DataSource{
|
||||||
|
Type: m.DS_GRAPHITE,
|
||||||
|
Url: "http://graphite:8086",
|
||||||
|
JsonData: json,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := &middleware.Context{}
|
||||||
|
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||||
|
|
||||||
|
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||||
|
req := http.Request{URL: requestUrl, Header: make(http.Header)}
|
||||||
|
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||||
|
req.Header.Set("Cookie", cookies)
|
||||||
|
|
||||||
|
proxy.getDirector()(&req)
|
||||||
|
|
||||||
|
Convey("Should clear all cookies", func() {
|
||||||
|
So(req.Header.Get("Cookie"), ShouldEqual, "")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When proxying a data source with keep cookies specified", func() {
|
||||||
|
plugin := &plugins.DataSourcePlugin{}
|
||||||
|
|
||||||
|
json, _ := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`))
|
||||||
|
|
||||||
|
ds := &m.DataSource{
|
||||||
|
Type: m.DS_GRAPHITE,
|
||||||
|
Url: "http://graphite:8086",
|
||||||
|
JsonData: json,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := &middleware.Context{}
|
||||||
|
proxy := NewDataSourceProxy(ds, plugin, ctx, "")
|
||||||
|
|
||||||
|
requestUrl, _ := url.Parse("http://grafana.com/sub")
|
||||||
|
req := http.Request{URL: requestUrl, Header: make(http.Header)}
|
||||||
|
cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test"
|
||||||
|
req.Header.Set("Cookie", cookies)
|
||||||
|
|
||||||
|
proxy.getDirector()(&req)
|
||||||
|
|
||||||
|
Convey("Should keep named cookies", func() {
|
||||||
|
So(req.Header.Get("Cookie"), ShouldEqual, "JSESSION_ID=test")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Convey("When interpolating string", func() {
|
Convey("When interpolating string", func() {
|
||||||
data := templateData{
|
data := templateData{
|
||||||
SecureJsonData: map[string]string{
|
SecureJsonData: map[string]string{
|
||||||
|
@ -10,25 +10,33 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func RenderToPng(c *middleware.Context) {
|
func RenderToPng(c *middleware.Context) {
|
||||||
queryReader := util.NewUrlQueryReader(c.Req.URL)
|
queryReader, err := util.NewUrlQueryReader(c.Req.URL)
|
||||||
|
if err != nil {
|
||||||
|
c.Handle(400, "Render parameters error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
|
queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery)
|
||||||
|
|
||||||
renderOpts := &renderer.RenderOpts{
|
renderOpts := &renderer.RenderOpts{
|
||||||
Path: c.Params("*") + queryParams,
|
Path: c.Params("*") + queryParams,
|
||||||
Width: queryReader.Get("width", "800"),
|
Width: queryReader.Get("width", "800"),
|
||||||
Height: queryReader.Get("height", "400"),
|
Height: queryReader.Get("height", "400"),
|
||||||
OrgId: c.OrgId,
|
|
||||||
Timeout: queryReader.Get("timeout", "60"),
|
Timeout: queryReader.Get("timeout", "60"),
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
UserId: c.UserId,
|
||||||
|
OrgRole: c.OrgRole,
|
||||||
Timezone: queryReader.Get("tz", ""),
|
Timezone: queryReader.Get("tz", ""),
|
||||||
|
Encoding: queryReader.Get("encoding", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
pngPath, err := renderer.RenderToPng(renderOpts)
|
pngPath, err := renderer.RenderToPng(renderOpts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil && err == renderer.ErrTimeout {
|
||||||
if err == renderer.ErrTimeout {
|
|
||||||
c.Handle(500, err.Error(), err)
|
c.Handle(500, err.Error(), err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
c.Handle(500, "Rendering failed.", err)
|
c.Handle(500, "Rendering failed.", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -14,27 +14,38 @@ func Search(c *middleware.Context) {
|
|||||||
tags := c.QueryStrings("tag")
|
tags := c.QueryStrings("tag")
|
||||||
starred := c.Query("starred")
|
starred := c.Query("starred")
|
||||||
limit := c.QueryInt("limit")
|
limit := c.QueryInt("limit")
|
||||||
|
dashboardType := c.Query("type")
|
||||||
|
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
limit = 1000
|
limit = 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
dbids := make([]int, 0)
|
dbids := make([]int64, 0)
|
||||||
for _, id := range c.QueryStrings("dashboardIds") {
|
for _, id := range c.QueryStrings("dashboardIds") {
|
||||||
dashboardId, err := strconv.Atoi(id)
|
dashboardId, err := strconv.ParseInt(id, 10, 64)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dbids = append(dbids, dashboardId)
|
dbids = append(dbids, dashboardId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
folderIds := make([]int64, 0)
|
||||||
|
for _, id := range c.QueryStrings("folderIds") {
|
||||||
|
folderId, err := strconv.ParseInt(id, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
folderIds = append(folderIds, folderId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
searchQuery := search.Query{
|
searchQuery := search.Query{
|
||||||
Title: query,
|
Title: query,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
UserId: c.UserId,
|
SignedInUser: c.SignedInUser,
|
||||||
Limit: limit,
|
Limit: limit,
|
||||||
IsStarred: starred == "true",
|
IsStarred: starred == "true",
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
DashboardIds: dbids,
|
DashboardIds: dbids,
|
||||||
|
Type: dashboardType,
|
||||||
|
FolderIds: folderIds,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bus.Dispatch(&searchQuery)
|
err := bus.Dispatch(&searchQuery)
|
||||||
|
97
pkg/api/team.go
Normal file
97
pkg/api/team.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// POST /api/teams
|
||||||
|
func CreateTeam(c *middleware.Context, cmd m.CreateTeamCommand) Response {
|
||||||
|
cmd.OrgId = c.OrgId
|
||||||
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
|
if err == m.ErrTeamNameTaken {
|
||||||
|
return ApiError(409, "Team name taken", err)
|
||||||
|
}
|
||||||
|
return ApiError(500, "Failed to create Team", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, &util.DynMap{
|
||||||
|
"teamId": cmd.Result.Id,
|
||||||
|
"message": "Team created",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/teams/:teamId
|
||||||
|
func UpdateTeam(c *middleware.Context, cmd m.UpdateTeamCommand) Response {
|
||||||
|
cmd.Id = c.ParamsInt64(":teamId")
|
||||||
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
|
if err == m.ErrTeamNameTaken {
|
||||||
|
return ApiError(400, "Team name taken", err)
|
||||||
|
}
|
||||||
|
return ApiError(500, "Failed to update Team", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiSuccess("Team updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/teams/:teamId
|
||||||
|
func DeleteTeamById(c *middleware.Context) Response {
|
||||||
|
if err := bus.Dispatch(&m.DeleteTeamCommand{Id: c.ParamsInt64(":teamId")}); err != nil {
|
||||||
|
if err == m.ErrTeamNotFound {
|
||||||
|
return ApiError(404, "Failed to delete Team. ID not found", nil)
|
||||||
|
}
|
||||||
|
return ApiError(500, "Failed to update Team", err)
|
||||||
|
}
|
||||||
|
return ApiSuccess("Team deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/teams/search
|
||||||
|
func SearchTeams(c *middleware.Context) Response {
|
||||||
|
perPage := c.QueryInt("perpage")
|
||||||
|
if perPage <= 0 {
|
||||||
|
perPage = 1000
|
||||||
|
}
|
||||||
|
page := c.QueryInt("page")
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
query := m.SearchTeamsQuery{
|
||||||
|
Query: c.Query("query"),
|
||||||
|
Name: c.Query("name"),
|
||||||
|
Page: page,
|
||||||
|
Limit: perPage,
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
return ApiError(500, "Failed to search Teams", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, team := range query.Result.Teams {
|
||||||
|
team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Result.Page = page
|
||||||
|
query.Result.PerPage = perPage
|
||||||
|
|
||||||
|
return Json(200, query.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/teams/:teamId
|
||||||
|
func GetTeamById(c *middleware.Context) Response {
|
||||||
|
query := m.GetTeamByIdQuery{Id: c.ParamsInt64(":teamId")}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
if err == m.ErrTeamNotFound {
|
||||||
|
return ApiError(404, "Team not found", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiError(500, "Failed to get Team", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, &query.Result)
|
||||||
|
}
|
49
pkg/api/team_members.go
Normal file
49
pkg/api/team_members.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/teams/:teamId/members
|
||||||
|
func GetTeamMembers(c *middleware.Context) Response {
|
||||||
|
query := m.GetTeamMembersQuery{TeamId: c.ParamsInt64(":teamId")}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
return ApiError(500, "Failed to get Team Members", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, member := range query.Result {
|
||||||
|
member.AvatarUrl = dtos.GetGravatarUrl(member.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, query.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/teams/:teamId/members
|
||||||
|
func AddTeamMember(c *middleware.Context, cmd m.AddTeamMemberCommand) Response {
|
||||||
|
cmd.TeamId = c.ParamsInt64(":teamId")
|
||||||
|
cmd.OrgId = c.OrgId
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
|
if err == m.ErrTeamMemberAlreadyAdded {
|
||||||
|
return ApiError(400, "User is already added to this team", err)
|
||||||
|
}
|
||||||
|
return ApiError(500, "Failed to add Member to Team", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, &util.DynMap{
|
||||||
|
"message": "Member added to Team",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/teams/:teamId/members/:userId
|
||||||
|
func RemoveTeamMember(c *middleware.Context) Response {
|
||||||
|
if err := bus.Dispatch(&m.RemoveTeamMemberCommand{TeamId: c.ParamsInt64(":teamId"), UserId: c.ParamsInt64(":userId")}); err != nil {
|
||||||
|
return ApiError(500, "Failed to remove Member from Team", err)
|
||||||
|
}
|
||||||
|
return ApiSuccess("Team Member removed")
|
||||||
|
}
|
71
pkg/api/team_test.go
Normal file
71
pkg/api/team_test.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTeamApiEndpoint(t *testing.T) {
|
||||||
|
Convey("Given two teams", t, func() {
|
||||||
|
mockResult := models.SearchTeamQueryResult{
|
||||||
|
Teams: []*models.SearchTeamDto{
|
||||||
|
{Name: "team1"},
|
||||||
|
{Name: "team2"},
|
||||||
|
},
|
||||||
|
TotalCount: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
Convey("When searching with no parameters", func() {
|
||||||
|
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
|
||||||
|
var sentLimit int
|
||||||
|
var sendPage int
|
||||||
|
bus.AddHandler("test", func(query *models.SearchTeamsQuery) error {
|
||||||
|
query.Result = mockResult
|
||||||
|
|
||||||
|
sentLimit = query.Limit
|
||||||
|
sendPage = query.Page
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.handlerFunc = SearchTeams
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||||
|
|
||||||
|
So(sentLimit, ShouldEqual, 1000)
|
||||||
|
So(sendPage, ShouldEqual, 1)
|
||||||
|
|
||||||
|
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2)
|
||||||
|
So(len(respJSON.Get("teams").MustArray()), ShouldEqual, 2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("When searching with page and perpage parameters", func() {
|
||||||
|
loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) {
|
||||||
|
var sentLimit int
|
||||||
|
var sendPage int
|
||||||
|
bus.AddHandler("test", func(query *models.SearchTeamsQuery) error {
|
||||||
|
query.Result = mockResult
|
||||||
|
|
||||||
|
sentLimit = query.Limit
|
||||||
|
sendPage = query.Page
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.handlerFunc = SearchTeams
|
||||||
|
sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec()
|
||||||
|
|
||||||
|
So(sentLimit, ShouldEqual, 10)
|
||||||
|
So(sendPage, ShouldEqual, 2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
@ -219,7 +220,7 @@ func SearchUsers(c *middleware.Context) Response {
|
|||||||
return Json(200, query.Result.Users)
|
return Json(200, query.Result.Users)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/search
|
// GET /api/users/search
|
||||||
func SearchUsersWithPaging(c *middleware.Context) Response {
|
func SearchUsersWithPaging(c *middleware.Context) Response {
|
||||||
query, err := searchUser(c)
|
query, err := searchUser(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -247,6 +248,10 @@ func searchUser(c *middleware.Context) (*m.SearchUsersQuery, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, user := range query.Result.Users {
|
||||||
|
user.AvatarUrl = dtos.GetGravatarUrl(user.Email)
|
||||||
|
}
|
||||||
|
|
||||||
query.Result.Page = page
|
query.Result.Page = page
|
||||||
query.Result.PerPage = perPage
|
query.Result.PerPage = perPage
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||||
@ -30,7 +30,7 @@ import (
|
|||||||
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
|
_ "github.com/grafana/grafana/pkg/tsdb/testdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "4.6.0"
|
var version = "5.0.0"
|
||||||
var commit = "NA"
|
var commit = "NA"
|
||||||
var buildstamp string
|
var buildstamp string
|
||||||
var build_date string
|
var build_date string
|
||||||
@ -40,9 +40,6 @@ var homePath = flag.String("homepath", "", "path to grafana install/home path, d
|
|||||||
var pidFile = flag.String("pidfile", "", "path to pid file")
|
var pidFile = flag.String("pidfile", "", "path to pid file")
|
||||||
var exitChan = make(chan int)
|
var exitChan = make(chan int)
|
||||||
|
|
||||||
func init() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
v := flag.Bool("v", false, "prints current version and exits")
|
v := flag.Bool("v", false, "prints current version and exits")
|
||||||
profile := flag.Bool("profile", false, "Turn on pprof profiling")
|
profile := flag.Bool("profile", false, "Turn on pprof profiling")
|
||||||
@ -82,12 +79,28 @@ func main() {
|
|||||||
setting.BuildStamp = buildstampInt64
|
setting.BuildStamp = buildstampInt64
|
||||||
|
|
||||||
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
|
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
|
||||||
|
shutdownCompleted := make(chan int)
|
||||||
server := NewGrafanaServer()
|
server := NewGrafanaServer()
|
||||||
server.Start()
|
|
||||||
|
go listenToSystemSignals(server, shutdownCompleted)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
code := 0
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
log.Error2("Startup failed", "error", err)
|
||||||
|
code = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func listenToSystemSignals(server models.GrafanaServer) {
|
exitChan <- code
|
||||||
|
}()
|
||||||
|
|
||||||
|
code := <-shutdownCompleted
|
||||||
|
log.Info2("Grafana shutdown completed.", "code", code)
|
||||||
|
log.Close()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) {
|
||||||
signalChan := make(chan os.Signal, 1)
|
signalChan := make(chan os.Signal, 1)
|
||||||
ignoreChan := make(chan os.Signal, 1)
|
ignoreChan := make(chan os.Signal, 1)
|
||||||
code := 0
|
code := 0
|
||||||
@ -97,10 +110,12 @@ func listenToSystemSignals(server models.GrafanaServer) {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case sig := <-signalChan:
|
case sig := <-signalChan:
|
||||||
// Stops trace if profiling has been enabled
|
trace.Stop() // Stops trace if profiling has been enabled
|
||||||
trace.Stop()
|
|
||||||
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
|
server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
|
||||||
|
shutdownCompleted <- 0
|
||||||
case code = <-exitChan:
|
case code = <-exitChan:
|
||||||
|
trace.Stop() // Stops trace if profiling has been enabled
|
||||||
server.Shutdown(code, "startup error")
|
server.Shutdown(code, "startup error")
|
||||||
|
shutdownCompleted <- code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
|
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
@ -18,7 +19,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/login"
|
"github.com/grafana/grafana/pkg/login"
|
||||||
"github.com/grafana/grafana/pkg/metrics"
|
"github.com/grafana/grafana/pkg/metrics"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/cleanup"
|
"github.com/grafana/grafana/pkg/services/cleanup"
|
||||||
@ -31,7 +31,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/tracing"
|
"github.com/grafana/grafana/pkg/tracing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewGrafanaServer() models.GrafanaServer {
|
func NewGrafanaServer() *GrafanaServerImpl {
|
||||||
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
||||||
childRoutines, childCtx := errgroup.WithContext(rootCtx)
|
childRoutines, childCtx := errgroup.WithContext(rootCtx)
|
||||||
|
|
||||||
@ -52,9 +52,7 @@ type GrafanaServerImpl struct {
|
|||||||
httpServer *api.HttpServer
|
httpServer *api.HttpServer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GrafanaServerImpl) Start() {
|
func (g *GrafanaServerImpl) Start() error {
|
||||||
go listenToSystemSignals(g)
|
|
||||||
|
|
||||||
g.initLogging()
|
g.initLogging()
|
||||||
g.writePIDFile()
|
g.writePIDFile()
|
||||||
|
|
||||||
@ -66,17 +64,13 @@ func (g *GrafanaServerImpl) Start() {
|
|||||||
social.NewOAuthService()
|
social.NewOAuthService()
|
||||||
plugins.Init()
|
plugins.Init()
|
||||||
|
|
||||||
if err := provisioning.StartUp(setting.DatasourcesPath); err != nil {
|
if err := provisioning.Init(g.context, setting.HomePath, setting.Cfg); err != nil {
|
||||||
logger.Error("Failed to provision Grafana from config", "error", err)
|
return fmt.Errorf("Failed to provision Grafana from config. error: %v", err)
|
||||||
g.Shutdown(1, "Startup failed")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closer, err := tracing.Init(setting.Cfg)
|
closer, err := tracing.Init(setting.Cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.log.Error("Tracing settings is not valid", "error", err)
|
return fmt.Errorf("Tracing settings is not valid. error: %v", err)
|
||||||
g.Shutdown(1, "Startup failed")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer closer.Close()
|
defer closer.Close()
|
||||||
|
|
||||||
@ -91,12 +85,12 @@ func (g *GrafanaServerImpl) Start() {
|
|||||||
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
|
g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) })
|
||||||
|
|
||||||
if err = notifications.Init(); err != nil {
|
if err = notifications.Init(); err != nil {
|
||||||
g.log.Error("Notification service failed to initialize", "error", err)
|
return fmt.Errorf("Notification service failed to initialize. error: %v", err)
|
||||||
g.Shutdown(1, "Startup failed")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g.startHttpServer()
|
sendSystemdNotification("READY=1")
|
||||||
|
|
||||||
|
return g.startHttpServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSql() {
|
func initSql() {
|
||||||
@ -120,16 +114,16 @@ func (g *GrafanaServerImpl) initLogging() {
|
|||||||
setting.LogConfigurationInfo()
|
setting.LogConfigurationInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GrafanaServerImpl) startHttpServer() {
|
func (g *GrafanaServerImpl) startHttpServer() error {
|
||||||
g.httpServer = api.NewHttpServer()
|
g.httpServer = api.NewHttpServer()
|
||||||
|
|
||||||
err := g.httpServer.Start(g.context)
|
err := g.httpServer.Start(g.context)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.log.Error("Fail to start server", "error", err)
|
return fmt.Errorf("Fail to start server. error: %v", err)
|
||||||
g.Shutdown(1, "Startup failed")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||||
@ -142,10 +136,9 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
|||||||
|
|
||||||
g.shutdownFn()
|
g.shutdownFn()
|
||||||
err = g.childRoutines.Wait()
|
err = g.childRoutines.Wait()
|
||||||
|
if err != nil && err != context.Canceled {
|
||||||
g.log.Info("Shutdown completed", "reason", err)
|
g.log.Error("Server shutdown completed with an error", "error", err)
|
||||||
log.Close()
|
}
|
||||||
os.Exit(code)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GrafanaServerImpl) writePIDFile() {
|
func (g *GrafanaServerImpl) writePIDFile() {
|
||||||
@ -169,3 +162,28 @@ func (g *GrafanaServerImpl) writePIDFile() {
|
|||||||
|
|
||||||
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
|
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendSystemdNotification(state string) error {
|
||||||
|
notifySocket := os.Getenv("NOTIFY_SOCKET")
|
||||||
|
|
||||||
|
if notifySocket == "" {
|
||||||
|
return fmt.Errorf("NOTIFY_SOCKET environment variable empty or unset.")
|
||||||
|
}
|
||||||
|
|
||||||
|
socketAddr := &net.UnixAddr{
|
||||||
|
Name: notifySocket,
|
||||||
|
Net: "unixgram",
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.Write([]byte(state))
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
320
pkg/components/imguploader/azureblobuploader.go
Normal file
320
pkg/components/imguploader/azureblobuploader.go
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
package imguploader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AzureBlobUploader struct {
|
||||||
|
account_name string
|
||||||
|
account_key string
|
||||||
|
container_name string
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAzureBlobUploader(account_name string, account_key string, container_name string) *AzureBlobUploader {
|
||||||
|
return &AzureBlobUploader{
|
||||||
|
account_name: account_name,
|
||||||
|
account_key: account_key,
|
||||||
|
container_name: container_name,
|
||||||
|
log: log.New("azureBlobUploader"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive path of image on disk and return azure blob url
|
||||||
|
func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (string, error) {
|
||||||
|
// setup client
|
||||||
|
blob := NewStorageClient(az.account_name, az.account_key)
|
||||||
|
|
||||||
|
file, err := os.Open(imageDiskPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
randomFileName := util.GetRandomString(30) + ".png"
|
||||||
|
// upload image
|
||||||
|
az.log.Debug("Uploading image to azure_blob", "conatiner_name", az.container_name, "blob_name", randomFileName)
|
||||||
|
resp, err := blob.FileUpload(az.container_name, randomFileName, file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode > 400 && resp.StatusCode < 600 {
|
||||||
|
body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||||
|
aerr := &Error{
|
||||||
|
Code: resp.StatusCode,
|
||||||
|
Status: resp.Status,
|
||||||
|
Body: body,
|
||||||
|
Header: resp.Header,
|
||||||
|
}
|
||||||
|
aerr.parseXML()
|
||||||
|
resp.Body.Close()
|
||||||
|
return "", aerr
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", az.account_name, az.container_name, randomFileName)
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AZURE LIBRARY
|
||||||
|
type Blobs struct {
|
||||||
|
XMLName xml.Name `xml:"EnumerationResults"`
|
||||||
|
Items []Blob `xml:"Blobs>Blob"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Blob struct {
|
||||||
|
Name string `xml:"Name"`
|
||||||
|
Property Property `xml:"Properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Property struct {
|
||||||
|
LastModified string `xml:"Last-Modified"`
|
||||||
|
Etag string `xml:"Etag"`
|
||||||
|
ContentLength int `xml:"Content-Length"`
|
||||||
|
ContentType string `xml:"Content-Type"`
|
||||||
|
BlobType string `xml:"BlobType"`
|
||||||
|
LeaseStatus string `xml:"LeaseStatus"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Code int
|
||||||
|
Status string
|
||||||
|
Body []byte
|
||||||
|
Header http.Header
|
||||||
|
|
||||||
|
AzureCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return fmt.Sprintf("status %d: %s", e.Code, e.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) parseXML() {
|
||||||
|
var xe xmlError
|
||||||
|
_ = xml.NewDecoder(bytes.NewReader(e.Body)).Decode(&xe)
|
||||||
|
e.AzureCode = xe.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlError struct {
|
||||||
|
XMLName xml.Name `xml:"Error"`
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ms_date_layout = "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||||
|
const version = "2017-04-17"
|
||||||
|
|
||||||
|
var client = &http.Client{}
|
||||||
|
|
||||||
|
type StorageClient struct {
|
||||||
|
Auth *Auth
|
||||||
|
Transport http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StorageClient) transport() http.RoundTripper {
|
||||||
|
if c.Transport != nil {
|
||||||
|
return c.Transport
|
||||||
|
}
|
||||||
|
return http.DefaultTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorageClient(account, accessKey string) *StorageClient {
|
||||||
|
return &StorageClient{
|
||||||
|
Auth: &Auth{
|
||||||
|
account,
|
||||||
|
accessKey,
|
||||||
|
},
|
||||||
|
Transport: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StorageClient) absUrl(format string, a ...interface{}) string {
|
||||||
|
part := fmt.Sprintf(format, a...)
|
||||||
|
return fmt.Sprintf("https://%s.blob.core.windows.net/%s", c.Auth.Account, part)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyHeadersToRequest(req *http.Request, headers map[string]string) {
|
||||||
|
for k, v := range headers {
|
||||||
|
req.Header[k] = []string{v}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StorageClient) FileUpload(container, blobName string, body io.Reader) (*http.Response, error) {
|
||||||
|
blobName = escape(blobName)
|
||||||
|
extension := strings.ToLower(path.Ext(blobName))
|
||||||
|
contentType := mime.TypeByExtension(extension)
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
buf.ReadFrom(body)
|
||||||
|
req, err := http.NewRequest(
|
||||||
|
"PUT",
|
||||||
|
c.absUrl("%s/%s", container, blobName),
|
||||||
|
buf,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
copyHeadersToRequest(req, map[string]string{
|
||||||
|
"x-ms-blob-type": "BlockBlob",
|
||||||
|
"x-ms-date": time.Now().UTC().Format(ms_date_layout),
|
||||||
|
"x-ms-version": version,
|
||||||
|
"Accept-Charset": "UTF-8",
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"Content-Length": strconv.Itoa(buf.Len()),
|
||||||
|
})
|
||||||
|
|
||||||
|
c.Auth.SignRequest(req)
|
||||||
|
|
||||||
|
return c.transport().RoundTrip(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func escape(content string) string {
|
||||||
|
content = url.QueryEscape(content)
|
||||||
|
// the Azure's behavior uses %20 to represent whitespace instead of + (plus)
|
||||||
|
content = strings.Replace(content, "+", "%20", -1)
|
||||||
|
// the Azure's behavior uses slash instead of + %2F
|
||||||
|
content = strings.Replace(content, "%2F", "/", -1)
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
Account string
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) SignRequest(req *http.Request) {
|
||||||
|
strToSign := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s",
|
||||||
|
strings.ToUpper(req.Method),
|
||||||
|
tryget(req.Header, "Content-Encoding"),
|
||||||
|
tryget(req.Header, "Content-Language"),
|
||||||
|
tryget(req.Header, "Content-Length"),
|
||||||
|
tryget(req.Header, "Content-MD5"),
|
||||||
|
tryget(req.Header, "Content-Type"),
|
||||||
|
tryget(req.Header, "Date"),
|
||||||
|
tryget(req.Header, "If-Modified-Since"),
|
||||||
|
tryget(req.Header, "If-Match"),
|
||||||
|
tryget(req.Header, "If-None-Match"),
|
||||||
|
tryget(req.Header, "If-Unmodified-Since"),
|
||||||
|
tryget(req.Header, "Range"),
|
||||||
|
a.canonicalizedHeaders(req),
|
||||||
|
a.canonicalizedResource(req),
|
||||||
|
)
|
||||||
|
decodedKey, _ := base64.StdEncoding.DecodeString(a.Key)
|
||||||
|
|
||||||
|
sha256 := hmac.New(sha256.New, []byte(decodedKey))
|
||||||
|
sha256.Write([]byte(strToSign))
|
||||||
|
|
||||||
|
signature := base64.StdEncoding.EncodeToString(sha256.Sum(nil))
|
||||||
|
|
||||||
|
copyHeadersToRequest(req, map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("SharedKey %s:%s", a.Account, signature),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryget(headers map[string][]string, key string) string {
|
||||||
|
// We default to empty string for "0" values to match server side behavior when generating signatures.
|
||||||
|
if len(headers[key]) > 0 { // && headers[key][0] != "0" { //&& key != "Content-Length" {
|
||||||
|
return headers[key][0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The following is copied ~95% verbatim from:
|
||||||
|
// http://github.com/loldesign/azure/ -> core/core.go
|
||||||
|
//
|
||||||
|
|
||||||
|
/*
|
||||||
|
Based on Azure docs:
|
||||||
|
Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
|
||||||
|
|
||||||
|
1) Retrieve all headers for the resource that begin with x-ms-, including the x-ms-date header.
|
||||||
|
2) Convert each HTTP header name to lowercase.
|
||||||
|
3) Sort the headers lexicographically by header name, in ascending order. Note that each header may appear only once in the string.
|
||||||
|
4) Unfold the string by replacing any breaking white space with a single space.
|
||||||
|
5) Trim any white space around the colon in the header.
|
||||||
|
6) Finally, append a new line character to each canonicalized header in the resulting list. Construct the CanonicalizedHeaders string by concatenating all headers in this list into a single string.
|
||||||
|
*/
|
||||||
|
func (a *Auth) canonicalizedHeaders(req *http.Request) string {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
for key, value := range req.Header {
|
||||||
|
lowerKey := strings.ToLower(key)
|
||||||
|
|
||||||
|
if strings.HasPrefix(lowerKey, "x-ms-") {
|
||||||
|
if buffer.Len() == 0 {
|
||||||
|
buffer.WriteString(fmt.Sprintf("%s:%s", lowerKey, value[0]))
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(fmt.Sprintf("\n%s:%s", lowerKey, value[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
splitted := strings.Split(buffer.String(), "\n")
|
||||||
|
sort.Strings(splitted)
|
||||||
|
|
||||||
|
return strings.Join(splitted, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Based on Azure docs
|
||||||
|
Link: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx#Constructing_Element
|
||||||
|
|
||||||
|
1) Beginning with an empty string (""), append a forward slash (/), followed by the name of the account that owns the resource being accessed.
|
||||||
|
2) Append the resource's encoded URI path, without any query parameters.
|
||||||
|
3) Retrieve all query parameters on the resource URI, including the comp parameter if it exists.
|
||||||
|
4) Convert all parameter names to lowercase.
|
||||||
|
5) Sort the query parameters lexicographically by parameter name, in ascending order.
|
||||||
|
6) URL-decode each query parameter name and value.
|
||||||
|
7) Append each query parameter name and value to the string in the following format, making sure to include the colon (:) between the name and the value:
|
||||||
|
parameter-name:parameter-value
|
||||||
|
|
||||||
|
8) If a query parameter has more than one value, sort all values lexicographically, then include them in a comma-separated list:
|
||||||
|
parameter-name:parameter-value-1,parameter-value-2,parameter-value-n
|
||||||
|
|
||||||
|
9) Append a new line character (\n) after each name-value pair.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
1) Avoid using the new line character (\n) in values for query parameters. If it must be used, ensure that it does not affect the format of the canonicalized resource string.
|
||||||
|
2) Avoid using commas in query parameter values.
|
||||||
|
*/
|
||||||
|
func (a *Auth) canonicalizedResource(req *http.Request) string {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
buffer.WriteString(fmt.Sprintf("/%s%s", a.Account, req.URL.Path))
|
||||||
|
queries := req.URL.Query()
|
||||||
|
|
||||||
|
for key, values := range queries {
|
||||||
|
sort.Strings(values)
|
||||||
|
buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ",")))
|
||||||
|
}
|
||||||
|
|
||||||
|
splitted := strings.Split(buffer.String(), "\n")
|
||||||
|
sort.Strings(splitted)
|
||||||
|
|
||||||
|
return strings.Join(splitted, "\n")
|
||||||
|
}
|
24
pkg/components/imguploader/azureblobuploader_test.go
Normal file
24
pkg/components/imguploader/azureblobuploader_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package imguploader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUploadToAzureBlob(t *testing.T) {
|
||||||
|
SkipConvey("[Integration test] for external_image_store.azure_blob", t, func() {
|
||||||
|
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||||
|
HomePath: "../../../",
|
||||||
|
})
|
||||||
|
|
||||||
|
uploader, _ := NewImageUploader()
|
||||||
|
|
||||||
|
path, err := uploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png")
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(path, ShouldNotEqual, "")
|
||||||
|
})
|
||||||
|
}
|
@ -3,6 +3,7 @@ package imguploader
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -76,6 +77,21 @@ func NewImageUploader() (ImageUploader, error) {
|
|||||||
path := gcssec.Key("path").MustString("")
|
path := gcssec.Key("path").MustString("")
|
||||||
|
|
||||||
return NewGCSUploader(keyFile, bucketName, path), nil
|
return NewGCSUploader(keyFile, bucketName, path), nil
|
||||||
|
case "azure_blob":
|
||||||
|
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
account_name := azureBlobSec.Key("account_name").MustString("")
|
||||||
|
account_key := azureBlobSec.Key("account_key").MustString("")
|
||||||
|
container_name := azureBlobSec.Key("container_name").MustString("")
|
||||||
|
|
||||||
|
return NewAzureBlobUploader(account_name, account_key, container_name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.ImageUploadProvider != "" {
|
||||||
|
log.Error2("The external image storage configuration is invalid", "unsupported provider", setting.ImageUploadProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NopImageUploader{}, nil
|
return NopImageUploader{}, nil
|
||||||
|
@ -119,5 +119,29 @@ func TestImageUploaderFactory(t *testing.T) {
|
|||||||
So(original.keyFile, ShouldEqual, "/etc/secrets/project-79a52befa3f6.json")
|
So(original.keyFile, ShouldEqual, "/etc/secrets/project-79a52befa3f6.json")
|
||||||
So(original.bucket, ShouldEqual, "project-grafana-east")
|
So(original.bucket, ShouldEqual, "project-grafana-east")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("AzureBlobUploader config", func() {
|
||||||
|
setting.NewConfigContext(&setting.CommandLineArgs{
|
||||||
|
HomePath: "../../../",
|
||||||
|
})
|
||||||
|
setting.ImageUploadProvider = "azure_blob"
|
||||||
|
|
||||||
|
Convey("with container name", func() {
|
||||||
|
azureBlobSec, err := setting.Cfg.GetSection("external_image_storage.azure_blob")
|
||||||
|
azureBlobSec.NewKey("account_name", "account_name")
|
||||||
|
azureBlobSec.NewKey("account_key", "account_key")
|
||||||
|
azureBlobSec.NewKey("container_name", "container_name")
|
||||||
|
|
||||||
|
uploader, err := NewImageUploader()
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
original, ok := uploader.(*AzureBlobUploader)
|
||||||
|
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
So(original.account_name, ShouldEqual, "account_name")
|
||||||
|
So(original.account_key, ShouldEqual, "account_key")
|
||||||
|
So(original.container_name, ShouldEqual, "container_name")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
@ -26,7 +27,11 @@ type RenderOpts struct {
|
|||||||
Height string
|
Height string
|
||||||
Timeout string
|
Timeout string
|
||||||
OrgId int64
|
OrgId int64
|
||||||
|
UserId int64
|
||||||
|
OrgRole models.RoleType
|
||||||
Timezone string
|
Timezone string
|
||||||
|
IsAlertContext bool
|
||||||
|
Encoding string
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
|
var ErrTimeout = errors.New("Timeout error. You can set timeout in seconds with &timeout url parameter")
|
||||||
@ -74,7 +79,11 @@ func RenderToPng(params *RenderOpts) (string, error) {
|
|||||||
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
|
pngPath, _ := filepath.Abs(filepath.Join(setting.ImagesDir, util.GetRandomString(20)))
|
||||||
pngPath = pngPath + ".png"
|
pngPath = pngPath + ".png"
|
||||||
|
|
||||||
renderKey := middleware.AddRenderAuthKey(params.OrgId)
|
orgRole := params.OrgRole
|
||||||
|
if params.IsAlertContext {
|
||||||
|
orgRole = models.ROLE_ADMIN
|
||||||
|
}
|
||||||
|
renderKey := middleware.AddRenderAuthKey(params.OrgId, params.UserId, orgRole)
|
||||||
defer middleware.RemoveRenderAuthKey(renderKey)
|
defer middleware.RemoveRenderAuthKey(renderKey)
|
||||||
|
|
||||||
timeout, err := strconv.Atoi(params.Timeout)
|
timeout, err := strconv.Atoi(params.Timeout)
|
||||||
@ -95,6 +104,10 @@ func RenderToPng(params *RenderOpts) (string, error) {
|
|||||||
"renderKey=" + renderKey,
|
"renderKey=" + renderKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.Encoding != "" {
|
||||||
|
cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", params.Encoding)}, cmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.Command(binPath, cmdArgs...)
|
cmd := exec.Command(binPath, cmdArgs...)
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ func initContextWithAnonymousUser(ctx *Context) bool {
|
|||||||
|
|
||||||
ctx.IsSignedIn = false
|
ctx.IsSignedIn = false
|
||||||
ctx.AllowAnonymous = true
|
ctx.AllowAnonymous = true
|
||||||
ctx.SignedInUser = &m.SignedInUser{}
|
ctx.SignedInUser = &m.SignedInUser{IsAnonymous: true}
|
||||||
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
|
ctx.OrgRole = m.RoleType(setting.AnonymousOrgRole)
|
||||||
ctx.OrgId = orgQuery.Result.Id
|
ctx.OrgId = orgQuery.Result.Id
|
||||||
ctx.OrgName = orgQuery.Result.Name
|
ctx.OrgName = orgQuery.Result.Name
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
@ -41,7 +42,7 @@ func OrgRedirect() macaron.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newUrl := setting.ToAbsUrl(fmt.Sprintf("%s?%s", c.Req.URL.Path, c.Req.URL.Query().Encode()))
|
newURL := setting.ToAbsUrl(fmt.Sprintf("%s?%s", strings.TrimPrefix(c.Req.URL.Path, "/"), c.Req.URL.Query().Encode()))
|
||||||
c.Redirect(newUrl, 302)
|
c.Redirect(newURL, 302)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,14 +33,15 @@ func initContextWithRenderAuth(ctx *Context) bool {
|
|||||||
|
|
||||||
type renderContextFunc func(key string) (string, error)
|
type renderContextFunc func(key string) (string, error)
|
||||||
|
|
||||||
func AddRenderAuthKey(orgId int64) string {
|
func AddRenderAuthKey(orgId int64, userId int64, orgRole m.RoleType) string {
|
||||||
renderKeysLock.Lock()
|
renderKeysLock.Lock()
|
||||||
|
|
||||||
key := util.GetRandomString(32)
|
key := util.GetRandomString(32)
|
||||||
|
|
||||||
renderKeys[key] = &m.SignedInUser{
|
renderKeys[key] = &m.SignedInUser{
|
||||||
OrgId: orgId,
|
OrgId: orgId,
|
||||||
OrgRole: m.ROLE_VIEWER,
|
OrgRole: orgRole,
|
||||||
|
UserId: userId,
|
||||||
}
|
}
|
||||||
|
|
||||||
renderKeysLock.Unlock()
|
renderKeysLock.Unlock()
|
||||||
|
95
pkg/models/dashboard_acl.go
Normal file
95
pkg/models/dashboard_acl.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PermissionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PERMISSION_VIEW PermissionType = 1 << iota
|
||||||
|
PERMISSION_EDIT
|
||||||
|
PERMISSION_ADMIN
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p PermissionType) String() string {
|
||||||
|
names := map[int]string{
|
||||||
|
int(PERMISSION_VIEW): "View",
|
||||||
|
int(PERMISSION_EDIT): "Edit",
|
||||||
|
int(PERMISSION_ADMIN): "Admin",
|
||||||
|
}
|
||||||
|
return names[int(p)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typed errors
|
||||||
|
var (
|
||||||
|
ErrDashboardAclInfoMissing = errors.New("User id and team id cannot both be empty for a dashboard permission.")
|
||||||
|
ErrDashboardPermissionDashboardEmpty = errors.New("Dashboard Id must be greater than zero for a dashboard permission.")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dashboard ACL model
|
||||||
|
type DashboardAcl struct {
|
||||||
|
Id int64
|
||||||
|
OrgId int64
|
||||||
|
DashboardId int64
|
||||||
|
|
||||||
|
UserId int64
|
||||||
|
TeamId int64
|
||||||
|
Role *RoleType // pointer to be nullable
|
||||||
|
Permission PermissionType
|
||||||
|
|
||||||
|
Created time.Time
|
||||||
|
Updated time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardAclInfoDTO struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
OrgId int64 `json:"-"`
|
||||||
|
DashboardId int64 `json:"dashboardId"`
|
||||||
|
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
UserLogin string `json:"userLogin"`
|
||||||
|
UserEmail string `json:"userEmail"`
|
||||||
|
TeamId int64 `json:"teamId"`
|
||||||
|
Team string `json:"team"`
|
||||||
|
Role *RoleType `json:"role,omitempty"`
|
||||||
|
Permission PermissionType `json:"permission"`
|
||||||
|
PermissionName string `json:"permissionName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// COMMANDS
|
||||||
|
//
|
||||||
|
|
||||||
|
type UpdateDashboardAclCommand struct {
|
||||||
|
DashboardId int64
|
||||||
|
Items []*DashboardAcl
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetDashboardAclCommand struct {
|
||||||
|
DashboardId int64
|
||||||
|
OrgId int64
|
||||||
|
UserId int64
|
||||||
|
TeamId int64
|
||||||
|
Permission PermissionType
|
||||||
|
|
||||||
|
Result DashboardAcl
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveDashboardAclCommand struct {
|
||||||
|
AclId int64
|
||||||
|
OrgId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// QUERIES
|
||||||
|
//
|
||||||
|
type GetDashboardAclInfoListQuery struct {
|
||||||
|
DashboardId int64
|
||||||
|
OrgId int64
|
||||||
|
Result []*DashboardAclInfoDTO
|
||||||
|
}
|
21
pkg/models/dashboard_acl_test.go
Normal file
21
pkg/models/dashboard_acl_test.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDashboardAclModel(t *testing.T) {
|
||||||
|
|
||||||
|
Convey("When printing a PermissionType", t, func() {
|
||||||
|
view := PERMISSION_VIEW
|
||||||
|
printed := fmt.Sprint(view)
|
||||||
|
|
||||||
|
Convey("Should output a friendly name", func() {
|
||||||
|
So(printed, ShouldEqual, "View")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -16,6 +16,9 @@ var (
|
|||||||
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
|
ErrDashboardWithSameNameExists = errors.New("A dashboard with the same name already exists")
|
||||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||||
|
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
||||||
|
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
|
||||||
|
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdatePluginDashboardError struct {
|
type UpdatePluginDashboardError struct {
|
||||||
@ -47,6 +50,9 @@ type Dashboard struct {
|
|||||||
|
|
||||||
UpdatedBy int64
|
UpdatedBy int64
|
||||||
CreatedBy int64
|
CreatedBy int64
|
||||||
|
FolderId int64
|
||||||
|
IsFolder bool
|
||||||
|
HasAcl bool
|
||||||
|
|
||||||
Title string
|
Title string
|
||||||
Data *simplejson.Json
|
Data *simplejson.Json
|
||||||
@ -64,6 +70,15 @@ func NewDashboard(title string) *Dashboard {
|
|||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDashboardFolder creates a new dashboard folder
|
||||||
|
func NewDashboardFolder(title string) *Dashboard {
|
||||||
|
folder := NewDashboard(title)
|
||||||
|
folder.Data.Set("schemaVersion", 16)
|
||||||
|
folder.Data.Set("editable", true)
|
||||||
|
folder.Data.Set("hideControls", true)
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
|
||||||
// GetTags turns the tags in data json into go string array
|
// GetTags turns the tags in data json into go string array
|
||||||
func (dash *Dashboard) GetTags() []string {
|
func (dash *Dashboard) GetTags() []string {
|
||||||
return dash.Data.Get("tags").MustStringArray()
|
return dash.Data.Get("tags").MustStringArray()
|
||||||
@ -111,6 +126,8 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
|
|||||||
dash.UpdatedBy = userId
|
dash.UpdatedBy = userId
|
||||||
dash.OrgId = cmd.OrgId
|
dash.OrgId = cmd.OrgId
|
||||||
dash.PluginId = cmd.PluginId
|
dash.PluginId = cmd.PluginId
|
||||||
|
dash.IsFolder = cmd.IsFolder
|
||||||
|
dash.FolderId = cmd.FolderId
|
||||||
dash.UpdateSlug()
|
dash.UpdateSlug()
|
||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
@ -122,8 +139,12 @@ func (dash *Dashboard) GetString(prop string, defaultValue string) string {
|
|||||||
|
|
||||||
// UpdateSlug updates the slug
|
// UpdateSlug updates the slug
|
||||||
func (dash *Dashboard) UpdateSlug() {
|
func (dash *Dashboard) UpdateSlug() {
|
||||||
title := strings.ToLower(dash.Data.Get("title").MustString())
|
title := dash.Data.Get("title").MustString()
|
||||||
dash.Slug = slug.Make(title)
|
dash.Slug = SlugifyTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SlugifyTitle(title string) string {
|
||||||
|
return slug.Make(strings.ToLower(title))
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -138,12 +159,16 @@ type SaveDashboardCommand struct {
|
|||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
RestoredFrom int `json:"-"`
|
RestoredFrom int `json:"-"`
|
||||||
PluginId string `json:"-"`
|
PluginId string `json:"-"`
|
||||||
|
FolderId int64 `json:"folderId"`
|
||||||
|
IsFolder bool `json:"isFolder"`
|
||||||
|
|
||||||
|
UpdatedAt time.Time
|
||||||
|
|
||||||
Result *Dashboard
|
Result *Dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteDashboardCommand struct {
|
type DeleteDashboardCommand struct {
|
||||||
Slug string
|
Id int64
|
||||||
OrgId int64
|
OrgId int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,12 @@ func TestDashboardModel(t *testing.T) {
|
|||||||
So(dashboard.Slug, ShouldEqual, "grafana-play-home")
|
So(dashboard.Slug, ShouldEqual, "grafana-play-home")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Can slugify title", t, func() {
|
||||||
|
slug := SlugifyTitle("Grafana Play Home")
|
||||||
|
|
||||||
|
So(slug, ShouldEqual, "grafana-play-home")
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Given a dashboard json", t, func() {
|
Convey("Given a dashboard json", t, func() {
|
||||||
json := simplejson.New()
|
json := simplejson.New()
|
||||||
json.Set("title", "test dash")
|
json.Set("title", "test dash")
|
||||||
@ -28,4 +34,27 @@ func TestDashboardModel(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Given a new dashboard folder", t, func() {
|
||||||
|
json := simplejson.New()
|
||||||
|
json.Set("title", "test dash")
|
||||||
|
|
||||||
|
cmd := &SaveDashboardCommand{Dashboard: json, IsFolder: true}
|
||||||
|
dash := cmd.GetDashboardModel()
|
||||||
|
|
||||||
|
Convey("Should set IsFolder to true", func() {
|
||||||
|
So(dash.IsFolder, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Given a child dashboard", t, func() {
|
||||||
|
json := simplejson.New()
|
||||||
|
json.Set("title", "test dash")
|
||||||
|
|
||||||
|
cmd := &SaveDashboardCommand{Dashboard: json, FolderId: 1}
|
||||||
|
dash := cmd.GetDashboardModel()
|
||||||
|
|
||||||
|
Convey("Should set FolderId", func() {
|
||||||
|
So(dash.FolderId, ShouldEqual, 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -20,23 +20,23 @@ type RoleType string
|
|||||||
const (
|
const (
|
||||||
ROLE_VIEWER RoleType = "Viewer"
|
ROLE_VIEWER RoleType = "Viewer"
|
||||||
ROLE_EDITOR RoleType = "Editor"
|
ROLE_EDITOR RoleType = "Editor"
|
||||||
ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor"
|
|
||||||
ROLE_ADMIN RoleType = "Admin"
|
ROLE_ADMIN RoleType = "Admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r RoleType) IsValid() bool {
|
func (r RoleType) IsValid() bool {
|
||||||
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR
|
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r RoleType) Includes(other RoleType) bool {
|
func (r RoleType) Includes(other RoleType) bool {
|
||||||
if r == ROLE_ADMIN {
|
if r == ROLE_ADMIN {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR {
|
|
||||||
|
if r == ROLE_EDITOR {
|
||||||
return other != ROLE_ADMIN
|
return other != ROLE_ADMIN
|
||||||
}
|
}
|
||||||
|
|
||||||
return r == other
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RoleType) UnmarshalJSON(data []byte) error {
|
func (r *RoleType) UnmarshalJSON(data []byte) error {
|
||||||
@ -106,6 +106,7 @@ type OrgUserDTO struct {
|
|||||||
OrgId int64 `json:"orgId"`
|
OrgId int64 `json:"orgId"`
|
||||||
UserId int64 `json:"userId"`
|
UserId int64 `json:"userId"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
Login string `json:"login"`
|
Login string `json:"login"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
type GrafanaServer interface {
|
|
||||||
Start()
|
|
||||||
Shutdown(code int, reason string)
|
|
||||||
}
|
|
80
pkg/models/team.go
Normal file
80
pkg/models/team.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Typed errors
|
||||||
|
var (
|
||||||
|
ErrTeamNotFound = errors.New("Team not found")
|
||||||
|
ErrTeamNameTaken = errors.New("Team name is taken")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Team model
|
||||||
|
type Team struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
OrgId int64 `json:"orgId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// COMMANDS
|
||||||
|
|
||||||
|
type CreateTeamCommand struct {
|
||||||
|
Name string `json:"name" binding:"Required"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
OrgId int64 `json:"-"`
|
||||||
|
|
||||||
|
Result Team `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateTeamCommand struct {
|
||||||
|
Id int64
|
||||||
|
Name string
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteTeamCommand struct {
|
||||||
|
Id int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetTeamByIdQuery struct {
|
||||||
|
Id int64
|
||||||
|
Result *Team
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetTeamsByUserQuery struct {
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
Result []*Team `json:"teams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchTeamsQuery struct {
|
||||||
|
Query string
|
||||||
|
Name string
|
||||||
|
Limit int
|
||||||
|
Page int
|
||||||
|
OrgId int64
|
||||||
|
|
||||||
|
Result SearchTeamQueryResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchTeamDto struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
OrgId int64 `json:"orgId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
|
MemberCount int64 `json:"memberCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchTeamQueryResult struct {
|
||||||
|
TotalCount int64 `json:"totalCount"`
|
||||||
|
Teams []*SearchTeamDto `json:"teams"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PerPage int `json:"perPage"`
|
||||||
|
}
|
56
pkg/models/team_member.go
Normal file
56
pkg/models/team_member.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Typed errors
|
||||||
|
var (
|
||||||
|
ErrTeamMemberAlreadyAdded = errors.New("User is already added to this team")
|
||||||
|
)
|
||||||
|
|
||||||
|
// TeamMember model
|
||||||
|
type TeamMember struct {
|
||||||
|
Id int64
|
||||||
|
OrgId int64
|
||||||
|
TeamId int64
|
||||||
|
UserId int64
|
||||||
|
|
||||||
|
Created time.Time
|
||||||
|
Updated time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------
|
||||||
|
// COMMANDS
|
||||||
|
|
||||||
|
type AddTeamMemberCommand struct {
|
||||||
|
UserId int64 `json:"userId" binding:"Required"`
|
||||||
|
OrgId int64 `json:"-"`
|
||||||
|
TeamId int64 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemoveTeamMemberCommand struct {
|
||||||
|
UserId int64
|
||||||
|
TeamId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// QUERIES
|
||||||
|
|
||||||
|
type GetTeamMembersQuery struct {
|
||||||
|
TeamId int64
|
||||||
|
Result []*TeamMemberDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------
|
||||||
|
// Projections and DTOs
|
||||||
|
|
||||||
|
type TeamMemberDTO struct {
|
||||||
|
OrgId int64 `json:"orgId"`
|
||||||
|
TeamId int64 `json:"teamId"`
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Login string `json:"login"`
|
||||||
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
|
}
|
@ -160,7 +160,9 @@ type SignedInUser struct {
|
|||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
ApiKeyId int64
|
ApiKeyId int64
|
||||||
|
OrgCount int
|
||||||
IsGrafanaAdmin bool
|
IsGrafanaAdmin bool
|
||||||
|
IsAnonymous bool
|
||||||
HelpFlags1 HelpFlags1
|
HelpFlags1 HelpFlags1
|
||||||
LastSeenAt time.Time
|
LastSeenAt time.Time
|
||||||
}
|
}
|
||||||
@ -169,10 +171,28 @@ func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
|
|||||||
return u.UserId > 0 && time.Since(u.LastSeenAt) > time.Minute*5
|
return u.UserId > 0 && time.Since(u.LastSeenAt) > time.Minute*5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *SignedInUser) NameOrFallback() string {
|
||||||
|
if u.Name != "" {
|
||||||
|
return u.Name
|
||||||
|
} else if u.Login != "" {
|
||||||
|
return u.Login
|
||||||
|
} else {
|
||||||
|
return u.Email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateUserLastSeenAtCommand struct {
|
type UpdateUserLastSeenAtCommand struct {
|
||||||
UserId int64
|
UserId int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *SignedInUser) HasRole(role RoleType) bool {
|
||||||
|
if user.IsGrafanaAdmin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.OrgRole.Includes(role)
|
||||||
|
}
|
||||||
|
|
||||||
type UserProfileDTO struct {
|
type UserProfileDTO struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
@ -188,6 +208,7 @@ type UserSearchHitDTO struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Login string `json:"login"`
|
Login string `json:"login"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
AvatarUrl string `json:"avatarUrl"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
LastSeenAt time.Time `json:"lastSeenAt"`
|
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||||
LastSeenAtAge string `json:"lastSeenAtAge"`
|
LastSeenAtAge string `json:"lastSeenAtAge"`
|
||||||
|
@ -69,6 +69,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
|||||||
UserId: cmd.UserId,
|
UserId: cmd.UserId,
|
||||||
Overwrite: cmd.Overwrite,
|
Overwrite: cmd.Overwrite,
|
||||||
PluginId: cmd.PluginId,
|
PluginId: cmd.PluginId,
|
||||||
|
FolderId: dashboard.FolderId,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&saveCmd); err != nil {
|
if err := bus.Dispatch(&saveCmd); err != nil {
|
||||||
|
@ -13,16 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDashboardImport(t *testing.T) {
|
func TestDashboardImport(t *testing.T) {
|
||||||
|
pluginScenario("When importing a plugin dashboard", t, func() {
|
||||||
Convey("When importing plugin dashboard", t, func() {
|
|
||||||
setting.Cfg = ini.Empty()
|
|
||||||
sec, _ := setting.Cfg.NewSection("plugin.test-app")
|
|
||||||
sec.NewKey("path", "../../tests/test-app")
|
|
||||||
err := Init()
|
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
var importedDash *m.Dashboard
|
var importedDash *m.Dashboard
|
||||||
|
|
||||||
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
|
bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error {
|
||||||
importedDash = cmd.GetDashboardModel()
|
importedDash = cmd.GetDashboardModel()
|
||||||
cmd.Result = importedDash
|
cmd.Result = importedDash
|
||||||
@ -39,7 +32,7 @@ func TestDashboardImport(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ImportDashboard(&cmd)
|
err := ImportDashboard(&cmd)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
Convey("should install dashboard", func() {
|
Convey("should install dashboard", func() {
|
||||||
@ -92,3 +85,16 @@ func TestDashboardImport(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pluginScenario(desc string, t *testing.T, fn func()) {
|
||||||
|
Convey("Given a plugin", t, func() {
|
||||||
|
setting.Cfg = ini.Empty()
|
||||||
|
sec, _ := setting.Cfg.NewSection("plugin.test-app")
|
||||||
|
sec.NewKey("path", "../../tests/test-app")
|
||||||
|
err := Init()
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
Convey(desc, fn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -15,6 +15,7 @@ type PluginDashboardInfoDTO struct {
|
|||||||
Imported bool `json:"imported"`
|
Imported bool `json:"imported"`
|
||||||
ImportedUri string `json:"importedUri"`
|
ImportedUri string `json:"importedUri"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
|
DashboardId int64 `json:"dashboardId"`
|
||||||
ImportedRevision int64 `json:"importedRevision"`
|
ImportedRevision int64 `json:"importedRevision"`
|
||||||
Revision int64 `json:"revision"`
|
Revision int64 `json:"revision"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
@ -60,6 +61,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
|
|||||||
// find existing dashboard
|
// find existing dashboard
|
||||||
for _, existingDash := range query.Result {
|
for _, existingDash := range query.Result {
|
||||||
if existingDash.Slug == dashboard.Slug {
|
if existingDash.Slug == dashboard.Slug {
|
||||||
|
res.DashboardId = existingDash.Id
|
||||||
res.Imported = true
|
res.Imported = true
|
||||||
res.ImportedUri = "db/" + existingDash.Slug
|
res.ImportedUri = "db/" + existingDash.Slug
|
||||||
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
|
res.ImportedRevision = existingDash.Data.Get("revision").MustInt64(1)
|
||||||
@ -75,6 +77,7 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
|
|||||||
if _, exists := existingMatches[dash.Id]; !exists {
|
if _, exists := existingMatches[dash.Id]; !exists {
|
||||||
result = append(result, &PluginDashboardInfoDTO{
|
result = append(result, &PluginDashboardInfoDTO{
|
||||||
Slug: dash.Slug,
|
Slug: dash.Slug,
|
||||||
|
DashboardId: dash.Id,
|
||||||
Removed: true,
|
Removed: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ func syncPluginDashboards(pluginDef *PluginBase, orgId int64) {
|
|||||||
if dash.Removed {
|
if dash.Removed {
|
||||||
plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
|
plog.Info("Deleting plugin dashboard", "pluginId", pluginDef.Id, "dashboard", dash.Slug)
|
||||||
|
|
||||||
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Slug: dash.Slug}
|
deleteCmd := m.DeleteDashboardCommand{OrgId: orgId, Id: dash.DashboardId}
|
||||||
if err := bus.Dispatch(&deleteCmd); err != nil {
|
if err := bus.Dispatch(&deleteCmd); err != nil {
|
||||||
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
|
plog.Error("Failed to auto update app dashboard", "pluginId", pluginDef.Id, "error", err)
|
||||||
return
|
return
|
||||||
@ -124,7 +124,7 @@ func handlePluginStateChanged(event *m.PluginStateChangedEvent) error {
|
|||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
for _, dash := range query.Result {
|
for _, dash := range query.Result {
|
||||||
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Slug: dash.Slug}
|
deleteCmd := m.DeleteDashboardCommand{OrgId: dash.OrgId, Id: dash.Id}
|
||||||
|
|
||||||
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
plog.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
|
||||||
|
|
||||||
|
@ -75,14 +75,6 @@ func (c *EvalContext) ShouldUpdateAlertState() bool {
|
|||||||
return c.Rule.State != c.PrevAlertState
|
return c.Rule.State != c.PrevAlertState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *EvalContext) ShouldSendNotification() bool {
|
|
||||||
if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *EvalContext) GetDurationMs() float64 {
|
func (a *EvalContext) GetDurationMs() float64 {
|
||||||
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
|
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
|
||||||
}
|
}
|
||||||
|
@ -28,21 +28,5 @@ func TestAlertingEvalContext(t *testing.T) {
|
|||||||
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
|
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should send notifications", func() {
|
|
||||||
Convey("pending -> ok", func() {
|
|
||||||
ctx.PrevAlertState = models.AlertStatePending
|
|
||||||
ctx.Rule.State = models.AlertStateOK
|
|
||||||
|
|
||||||
So(ctx.ShouldSendNotification(), ShouldBeFalse)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("ok -> alerting", func() {
|
|
||||||
ctx.PrevAlertState = models.AlertStateOK
|
|
||||||
ctx.Rule.State = models.AlertStateAlerting
|
|
||||||
|
|
||||||
So(ctx.ShouldSendNotification(), ShouldBeTrue)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,11 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
firing = cr.Firing
|
||||||
|
noDataFound = cr.NoDataFound
|
||||||
|
}
|
||||||
|
|
||||||
// calculating Firing based on operator
|
// calculating Firing based on operator
|
||||||
if cr.Operator == "or" {
|
if cr.Operator == "or" {
|
||||||
firing = firing || cr.Firing
|
firing = firing || cr.Firing
|
||||||
|
@ -36,6 +36,16 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
|||||||
So(context.ConditionEvals, ShouldEqual, "true = true")
|
So(context.ConditionEvals, ShouldEqual, "true = true")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Show return triggered with single passing condition2", func() {
|
||||||
|
context := NewEvalContext(context.TODO(), &Rule{
|
||||||
|
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.Eval(context)
|
||||||
|
So(context.Firing, ShouldEqual, true)
|
||||||
|
So(context.ConditionEvals, ShouldEqual, "true = true")
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Show return false with not passing asdf", func() {
|
Convey("Show return false with not passing asdf", func() {
|
||||||
context := NewEvalContext(context.TODO(), &Rule{
|
context := NewEvalContext(context.TODO(), &Rule{
|
||||||
Conditions: []Condition{
|
Conditions: []Condition{
|
||||||
@ -131,6 +141,33 @@ func TestAlertingEvaluationHandler(t *testing.T) {
|
|||||||
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
|
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Should return false if no condition is firing using OR operator", func() {
|
||||||
|
context := NewEvalContext(context.TODO(), &Rule{
|
||||||
|
Conditions: []Condition{
|
||||||
|
&conditionStub{firing: false, operator: "or"},
|
||||||
|
&conditionStub{firing: false, operator: "or"},
|
||||||
|
&conditionStub{firing: false, operator: "or"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.Eval(context)
|
||||||
|
So(context.Firing, ShouldEqual, false)
|
||||||
|
So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should retuasdfrn no data if one condition has nodata", func() {
|
||||||
|
context := NewEvalContext(context.TODO(), &Rule{
|
||||||
|
Conditions: []Condition{
|
||||||
|
&conditionStub{operator: "or", noData: false},
|
||||||
|
&conditionStub{operator: "or", noData: false},
|
||||||
|
&conditionStub{operator: "or", noData: false},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.Eval(context)
|
||||||
|
So(context.NoDataFound, ShouldBeFalse)
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Should return no data if one condition has nodata", func() {
|
Convey("Should return no data if one condition has nodata", func() {
|
||||||
context := NewEvalContext(context.TODO(), &Rule{
|
context := NewEvalContext(context.TODO(), &Rule{
|
||||||
Conditions: []Condition{
|
Conditions: []Condition{
|
||||||
|
@ -69,19 +69,10 @@ func copyJson(in *simplejson.Json) (*simplejson.Json, error) {
|
|||||||
return simplejson.NewJson(rawJson)
|
return simplejson.NewJson(rawJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
func (e *DashAlertExtractor) GetAlertFromPanels(jsonWithPanels *simplejson.Json) ([]*m.Alert, error) {
|
||||||
e.log.Debug("GetAlerts")
|
|
||||||
|
|
||||||
dashboardJson, err := copyJson(e.Dash.Data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
alerts := make([]*m.Alert, 0)
|
alerts := make([]*m.Alert, 0)
|
||||||
for _, rowObj := range dashboardJson.Get("rows").MustArray() {
|
|
||||||
row := simplejson.NewFromAny(rowObj)
|
|
||||||
|
|
||||||
for _, panelObj := range row.Get("panels").MustArray() {
|
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
||||||
panel := simplejson.NewFromAny(panelObj)
|
panel := simplejson.NewFromAny(panelObj)
|
||||||
jsonAlert, hasAlert := panel.CheckGet("alert")
|
jsonAlert, hasAlert := panel.CheckGet("alert")
|
||||||
|
|
||||||
@ -158,6 +149,40 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return alerts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
|
||||||
|
e.log.Debug("GetAlerts")
|
||||||
|
|
||||||
|
dashboardJson, err := copyJson(e.Dash.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
alerts := make([]*m.Alert, 0)
|
||||||
|
|
||||||
|
// We extract alerts from rows to be backwards compatible
|
||||||
|
// with the old dashboard json model.
|
||||||
|
rows := dashboardJson.Get("rows").MustArray()
|
||||||
|
if len(rows) > 0 {
|
||||||
|
for _, rowObj := range rows {
|
||||||
|
row := simplejson.NewFromAny(rowObj)
|
||||||
|
a, err := e.GetAlertFromPanels(row)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
alerts = append(alerts, a...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a, err := e.GetAlertFromPanels(dashboardJson)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
alerts = append(alerts, a...)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts))
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -18,10 +18,6 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
return &FakeCondition{}, nil
|
return &FakeCondition{}, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
setting.NewConfigContext(&setting.CommandLineArgs{
|
|
||||||
HomePath: "../../../",
|
|
||||||
})
|
|
||||||
|
|
||||||
// mock data
|
// mock data
|
||||||
defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
|
defaultDs := &m.DataSource{Id: 12, OrgId: 1, Name: "I am default", IsDefault: true}
|
||||||
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
|
graphite2Ds := &m.DataSource{Id: 15, OrgId: 1, Name: "graphite2"}
|
||||||
@ -45,70 +41,8 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
json := `
|
json, err := ioutil.ReadFile("./test-data/graphite-alert.json")
|
||||||
{
|
So(err, ShouldBeNil)
|
||||||
"id": 57,
|
|
||||||
"title": "Graphite 4",
|
|
||||||
"originalTitle": "Graphite 4",
|
|
||||||
"tags": ["graphite"],
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"title": "Active desktop users",
|
|
||||||
"editable": true,
|
|
||||||
"type": "graph",
|
|
||||||
"id": 3,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"refId": "A",
|
|
||||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"datasource": null,
|
|
||||||
"alert": {
|
|
||||||
"name": "name1",
|
|
||||||
"message": "desc1",
|
|
||||||
"handler": 1,
|
|
||||||
"frequency": "60s",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"type": "query",
|
|
||||||
"query": {"params": ["A", "5m", "now"]},
|
|
||||||
"reducer": {"type": "avg", "params": []},
|
|
||||||
"evaluator": {"type": ">", "params": [100]}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Active mobile users",
|
|
||||||
"id": 4,
|
|
||||||
"targets": [
|
|
||||||
{"refId": "A", "target": ""},
|
|
||||||
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
|
||||||
],
|
|
||||||
"datasource": "graphite2",
|
|
||||||
"alert": {
|
|
||||||
"name": "name2",
|
|
||||||
"message": "desc2",
|
|
||||||
"handler": 0,
|
|
||||||
"frequency": "60s",
|
|
||||||
"severity": "warning",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"type": "query",
|
|
||||||
"query": {"params": ["B", "5m", "now"]},
|
|
||||||
"reducer": {"type": "avg", "params": []},
|
|
||||||
"evaluator": {"type": ">", "params": [100]}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`
|
|
||||||
|
|
||||||
Convey("Extractor should not modify the original json", func() {
|
Convey("Extractor should not modify the original json", func() {
|
||||||
dashJson, err := simplejson.NewJson([]byte(json))
|
dashJson, err := simplejson.NewJson([]byte(json))
|
||||||
@ -201,69 +135,8 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("Panels missing id should return error", func() {
|
Convey("Panels missing id should return error", func() {
|
||||||
panelWithoutId := `
|
panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json")
|
||||||
{
|
So(err, ShouldBeNil)
|
||||||
"id": 57,
|
|
||||||
"title": "Graphite 4",
|
|
||||||
"originalTitle": "Graphite 4",
|
|
||||||
"tags": ["graphite"],
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"title": "Active desktop users",
|
|
||||||
"editable": true,
|
|
||||||
"type": "graph",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"refId": "A",
|
|
||||||
"target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"datasource": null,
|
|
||||||
"alert": {
|
|
||||||
"name": "name1",
|
|
||||||
"message": "desc1",
|
|
||||||
"handler": 1,
|
|
||||||
"frequency": "60s",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"type": "query",
|
|
||||||
"query": {"params": ["A", "5m", "now"]},
|
|
||||||
"reducer": {"type": "avg", "params": []},
|
|
||||||
"evaluator": {"type": ">", "params": [100]}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Active mobile users",
|
|
||||||
"id": 4,
|
|
||||||
"targets": [
|
|
||||||
{"refId": "A", "target": ""},
|
|
||||||
{"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}
|
|
||||||
],
|
|
||||||
"datasource": "graphite2",
|
|
||||||
"alert": {
|
|
||||||
"name": "name2",
|
|
||||||
"message": "desc2",
|
|
||||||
"handler": 0,
|
|
||||||
"frequency": "60s",
|
|
||||||
"severity": "warning",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"type": "query",
|
|
||||||
"query": {"params": ["B", "5m", "now"]},
|
|
||||||
"reducer": {"type": "avg", "params": []},
|
|
||||||
"evaluator": {"type": ">", "params": [100]}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`
|
|
||||||
|
|
||||||
dashJson, err := simplejson.NewJson([]byte(panelWithoutId))
|
dashJson, err := simplejson.NewJson([]byte(panelWithoutId))
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
@ -277,294 +150,31 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("Parse alerts from dashboard without rows", func() {
|
||||||
|
json, err := ioutil.ReadFile("./test-data/v5-dashboard.json")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dashJson, err := simplejson.NewJson(json)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
|
extractor := NewDashAlertExtractor(dash, 1)
|
||||||
|
|
||||||
|
alerts, err := extractor.GetAlerts()
|
||||||
|
|
||||||
|
Convey("Get rules without error", func() {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should have 2 alert rule", func() {
|
||||||
|
So(len(alerts), ShouldEqual, 2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Parse and validate dashboard containing influxdb alert", func() {
|
Convey("Parse and validate dashboard containing influxdb alert", func() {
|
||||||
|
json, err := ioutil.ReadFile("./test-data/influxdb-alert.json")
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
json2 := `{
|
dashJson, err := simplejson.NewJson(json)
|
||||||
"id": 4,
|
|
||||||
"title": "Influxdb",
|
|
||||||
"tags": [
|
|
||||||
"apa"
|
|
||||||
],
|
|
||||||
"style": "dark",
|
|
||||||
"timezone": "browser",
|
|
||||||
"editable": true,
|
|
||||||
"hideControls": false,
|
|
||||||
"sharedCrosshair": false,
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"collapse": false,
|
|
||||||
"editable": true,
|
|
||||||
"height": "450px",
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"alert": {
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"evaluator": {
|
|
||||||
"params": [
|
|
||||||
10
|
|
||||||
],
|
|
||||||
"type": "gt"
|
|
||||||
},
|
|
||||||
"query": {
|
|
||||||
"params": [
|
|
||||||
"B",
|
|
||||||
"5m",
|
|
||||||
"now"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"reducer": {
|
|
||||||
"params": [],
|
|
||||||
"type": "avg"
|
|
||||||
},
|
|
||||||
"type": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"frequency": "3s",
|
|
||||||
"handler": 1,
|
|
||||||
"name": "Influxdb",
|
|
||||||
"noDataState": "no_data",
|
|
||||||
"notifications": [
|
|
||||||
{
|
|
||||||
"id": 6
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"alerting": {},
|
|
||||||
"aliasColors": {
|
|
||||||
"logins.count.count": "#890F02"
|
|
||||||
},
|
|
||||||
"bars": false,
|
|
||||||
"datasource": "InfluxDB",
|
|
||||||
"editable": true,
|
|
||||||
"error": false,
|
|
||||||
"fill": 1,
|
|
||||||
"grid": {},
|
|
||||||
"id": 1,
|
|
||||||
"interval": ">10s",
|
|
||||||
"isNew": true,
|
|
||||||
"legend": {
|
|
||||||
"avg": false,
|
|
||||||
"current": false,
|
|
||||||
"max": false,
|
|
||||||
"min": false,
|
|
||||||
"show": true,
|
|
||||||
"total": false,
|
|
||||||
"values": false
|
|
||||||
},
|
|
||||||
"lines": true,
|
|
||||||
"linewidth": 2,
|
|
||||||
"links": [],
|
|
||||||
"nullPointMode": "connected",
|
|
||||||
"percentage": false,
|
|
||||||
"pointradius": 5,
|
|
||||||
"points": false,
|
|
||||||
"renderer": "flot",
|
|
||||||
"seriesOverrides": [],
|
|
||||||
"span": 10,
|
|
||||||
"stack": false,
|
|
||||||
"steppedLine": false,
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"dsType": "influxdb",
|
|
||||||
"groupBy": [
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"$interval"
|
|
||||||
],
|
|
||||||
"type": "time"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"datacenter"
|
|
||||||
],
|
|
||||||
"type": "tag"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"none"
|
|
||||||
],
|
|
||||||
"type": "fill"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hide": false,
|
|
||||||
"measurement": "logins.count",
|
|
||||||
"policy": "default",
|
|
||||||
"query": "SELECT 8 * count(\"value\") FROM \"logins.count\" WHERE $timeFilter GROUP BY time($interval), \"datacenter\" fill(none)",
|
|
||||||
"rawQuery": true,
|
|
||||||
"refId": "B",
|
|
||||||
"resultFormat": "time_series",
|
|
||||||
"select": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"value"
|
|
||||||
],
|
|
||||||
"type": "field"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"params": [],
|
|
||||||
"type": "count"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"tags": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dsType": "influxdb",
|
|
||||||
"groupBy": [
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"$interval"
|
|
||||||
],
|
|
||||||
"type": "time"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"null"
|
|
||||||
],
|
|
||||||
"type": "fill"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"hide": true,
|
|
||||||
"measurement": "cpu",
|
|
||||||
"policy": "default",
|
|
||||||
"refId": "A",
|
|
||||||
"resultFormat": "time_series",
|
|
||||||
"select": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"value"
|
|
||||||
],
|
|
||||||
"type": "field"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"params": [],
|
|
||||||
"type": "mean"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"params": [
|
|
||||||
"value"
|
|
||||||
],
|
|
||||||
"type": "field"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"params": [],
|
|
||||||
"type": "sum"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"tags": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": [
|
|
||||||
{
|
|
||||||
"colorMode": "critical",
|
|
||||||
"fill": true,
|
|
||||||
"line": true,
|
|
||||||
"op": "gt",
|
|
||||||
"value": 10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"timeFrom": null,
|
|
||||||
"timeShift": null,
|
|
||||||
"title": "Panel Title",
|
|
||||||
"tooltip": {
|
|
||||||
"msResolution": false,
|
|
||||||
"ordering": "alphabetical",
|
|
||||||
"shared": true,
|
|
||||||
"sort": 0,
|
|
||||||
"value_type": "cumulative"
|
|
||||||
},
|
|
||||||
"type": "graph",
|
|
||||||
"xaxis": {
|
|
||||||
"mode": "time",
|
|
||||||
"name": null,
|
|
||||||
"show": true,
|
|
||||||
"values": []
|
|
||||||
},
|
|
||||||
"yaxes": [
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "short",
|
|
||||||
"logBase": 1,
|
|
||||||
"max": null,
|
|
||||||
"min": null,
|
|
||||||
"show": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"editable": true,
|
|
||||||
"error": false,
|
|
||||||
"id": 2,
|
|
||||||
"isNew": true,
|
|
||||||
"limit": 10,
|
|
||||||
"links": [],
|
|
||||||
"show": "current",
|
|
||||||
"span": 2,
|
|
||||||
"stateFilter": [
|
|
||||||
"alerting"
|
|
||||||
],
|
|
||||||
"title": "Alert status",
|
|
||||||
"type": "alertlist"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Row"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": {
|
|
||||||
"from": "now-5m",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {
|
|
||||||
"now": true,
|
|
||||||
"refresh_intervals": [
|
|
||||||
"5s",
|
|
||||||
"10s",
|
|
||||||
"30s",
|
|
||||||
"1m",
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"30m",
|
|
||||||
"1h",
|
|
||||||
"2h",
|
|
||||||
"1d"
|
|
||||||
],
|
|
||||||
"time_options": [
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"1h",
|
|
||||||
"6h",
|
|
||||||
"12h",
|
|
||||||
"24h",
|
|
||||||
"2d",
|
|
||||||
"7d",
|
|
||||||
"30d"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"templating": {
|
|
||||||
"list": []
|
|
||||||
},
|
|
||||||
"annotations": {
|
|
||||||
"list": []
|
|
||||||
},
|
|
||||||
"schemaVersion": 13,
|
|
||||||
"version": 120,
|
|
||||||
"links": [],
|
|
||||||
"gnetId": null
|
|
||||||
}`
|
|
||||||
|
|
||||||
dashJson, err := simplejson.NewJson([]byte(json2))
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
extractor := NewDashAlertExtractor(dash, 1)
|
extractor := NewDashAlertExtractor(dash, 1)
|
||||||
|
@ -15,7 +15,7 @@ type Notifier interface {
|
|||||||
Notify(evalContext *EvalContext) error
|
Notify(evalContext *EvalContext) error
|
||||||
GetType() string
|
GetType() string
|
||||||
NeedsImage() bool
|
NeedsImage() bool
|
||||||
PassesFilter(rule *Rule) bool
|
ShouldNotify(evalContext *EvalContext) bool
|
||||||
|
|
||||||
GetNotifierId() int64
|
GetNotifierId() int64
|
||||||
GetIsDefault() bool
|
GetIsDefault() bool
|
||||||
|
@ -24,7 +24,7 @@ type NotifierPlugin struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NotificationService interface {
|
type NotificationService interface {
|
||||||
Send(context *EvalContext) error
|
SendIfNeeded(context *EvalContext) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNotificationService() NotificationService {
|
func NewNotificationService() NotificationService {
|
||||||
@ -41,14 +41,12 @@ func newNotificationService() *notificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notificationService) Send(context *EvalContext) error {
|
func (n *notificationService) SendIfNeeded(context *EvalContext) error {
|
||||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
|
|
||||||
|
|
||||||
if len(notifiers) == 0 {
|
if len(notifiers) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -67,7 +65,7 @@ func (n *notificationService) sendNotifications(context *EvalContext, notifiers
|
|||||||
|
|
||||||
for _, notifier := range notifiers {
|
for _, notifier := range notifiers {
|
||||||
not := notifier //avoid updating scope variable in go routine
|
not := notifier //avoid updating scope variable in go routine
|
||||||
n.log.Info("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault())
|
||||||
metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
|
metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc()
|
||||||
g.Go(func() error { return not.Notify(context) })
|
g.Go(func() error { return not.Notify(context) })
|
||||||
}
|
}
|
||||||
@ -86,6 +84,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
|||||||
Height: "400",
|
Height: "400",
|
||||||
Timeout: "30",
|
Timeout: "30",
|
||||||
OrgId: context.Rule.OrgId,
|
OrgId: context.Rule.OrgId,
|
||||||
|
IsAlertContext: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if slug, err := context.GetDashboardSlug(); err != nil {
|
if slug, err := context.GetDashboardSlug(); err != nil {
|
||||||
@ -109,7 +108,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
|
||||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||||
|
|
||||||
if err := bus.Dispatch(query); err != nil {
|
if err := bus.Dispatch(query); err != nil {
|
||||||
@ -121,7 +120,7 @@ func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64,
|
|||||||
if not, err := n.createNotifierFor(notification); err != nil {
|
if not, err := n.createNotifierFor(notification); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
if shouldUseNotification(not, context) {
|
if not.ShouldNotify(context) {
|
||||||
result = append(result, not)
|
result = append(result, not)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,18 +138,6 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
|
|||||||
return notifierPlugin.Factory(model)
|
return notifierPlugin.Factory(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
|
|
||||||
if !context.Firing {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if context.Error != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return notifier.PassesFilter(context.Rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||||
|
|
||||||
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
|
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
package alerting
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FakeNotifier struct {
|
|
||||||
FakeMatchResult bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fn *FakeNotifier) GetType() string {
|
|
||||||
return "FakeNotifier"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fn *FakeNotifier) NeedsImage() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *FakeNotifier) GetNotifierId() int64 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *FakeNotifier) GetIsDefault() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
|
|
||||||
|
|
||||||
func (fn *FakeNotifier) PassesFilter(rule *Rule) bool {
|
|
||||||
return fn.FakeMatchResult
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAlertNotificationExtraction(t *testing.T) {
|
|
||||||
|
|
||||||
Convey("Notifier tests", t, func() {
|
|
||||||
Convey("none firing alerts", func() {
|
|
||||||
ctx := &EvalContext{
|
|
||||||
Firing: false,
|
|
||||||
Rule: &Rule{
|
|
||||||
State: m.AlertStateAlerting,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
|
||||||
|
|
||||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("execution error cannot be ignored", func() {
|
|
||||||
ctx := &EvalContext{
|
|
||||||
Firing: true,
|
|
||||||
Error: fmt.Errorf("I used to be a programmer just like you"),
|
|
||||||
Rule: &Rule{
|
|
||||||
State: m.AlertStateOK,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
|
||||||
|
|
||||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("firing alert that match", func() {
|
|
||||||
ctx := &EvalContext{
|
|
||||||
Firing: true,
|
|
||||||
Rule: &Rule{
|
|
||||||
State: models.AlertStateAlerting,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
notifier := &FakeNotifier{FakeMatchResult: true}
|
|
||||||
|
|
||||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("firing alert that dont match", func() {
|
|
||||||
ctx := &EvalContext{
|
|
||||||
Firing: true,
|
|
||||||
Rule: &Rule{State: m.AlertStateOK},
|
|
||||||
}
|
|
||||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
|
||||||
|
|
||||||
So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
96
pkg/services/alerting/notifiers/alertmanager.go
Normal file
96
pkg/services/alerting/notifiers/alertmanager.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||||
|
Type: "prometheus-alertmanager",
|
||||||
|
Name: "Prometheus Alertmanager",
|
||||||
|
Description: "Sends alert to Prometheus Alertmanager",
|
||||||
|
Factory: NewAlertmanagerNotifier,
|
||||||
|
OptionsTemplate: `
|
||||||
|
<h3 class="page-heading">Alertmanager settings</h3>
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label width-10">Url</span>
|
||||||
|
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="http://localhost:9093"></input>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||||
|
url := model.Settings.Get("url").MustString()
|
||||||
|
if url == "" {
|
||||||
|
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AlertmanagerNotifier{
|
||||||
|
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||||
|
Url: url,
|
||||||
|
log: log.New("alerting.notifier.prometheus-alertmanager"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertmanagerNotifier struct {
|
||||||
|
NotifierBase
|
||||||
|
Url string
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool {
|
||||||
|
return evalContext.Rule.State == m.AlertStateAlerting
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *AlertmanagerNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||||
|
|
||||||
|
alerts := make([]interface{}, 0)
|
||||||
|
for _, match := range evalContext.EvalMatches {
|
||||||
|
alertJSON := simplejson.New()
|
||||||
|
alertJSON.Set("startsAt", evalContext.StartTime.UTC().Format(time.RFC3339))
|
||||||
|
|
||||||
|
if ruleUrl, err := evalContext.GetRuleUrl(); err == nil {
|
||||||
|
alertJSON.Set("generatorURL", ruleUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if evalContext.Rule.Message != "" {
|
||||||
|
alertJSON.SetPath([]string{"annotations", "description"}, evalContext.Rule.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := make(map[string]string)
|
||||||
|
if len(match.Tags) == 0 {
|
||||||
|
tags["metric"] = match.Metric
|
||||||
|
} else {
|
||||||
|
for k, v := range match.Tags {
|
||||||
|
tags[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags["alertname"] = evalContext.Rule.Name
|
||||||
|
alertJSON.Set("labels", tags)
|
||||||
|
|
||||||
|
alerts = append(alerts, alertJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyJSON := simplejson.NewFromAny(alerts)
|
||||||
|
body, _ := bodyJSON.MarshalJSON()
|
||||||
|
|
||||||
|
cmd := &m.SendWebhookSync{
|
||||||
|
Url: this.Url + "/api/v1/alerts",
|
||||||
|
HttpMethod: "POST",
|
||||||
|
Body: string(body),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||||
|
this.log.Error("Failed to send alertmanager", "error", err, "alertmanager", this.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
47
pkg/services/alerting/notifiers/alertmanager_test.go
Normal file
47
pkg/services/alerting/notifiers/alertmanager_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 := `{ }`
|
||||||
|
|
||||||
|
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||||
|
model := &m.AlertNotification{
|
||||||
|
Name: "alertmanager",
|
||||||
|
Type: "alertmanager",
|
||||||
|
Settings: settingsJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewAlertmanagerNotifier(model)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("from settings", func() {
|
||||||
|
json := `{ "url": "http://127.0.0.1:9093/" }`
|
||||||
|
|
||||||
|
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||||
|
model := &m.AlertNotification{
|
||||||
|
Name: "alertmanager",
|
||||||
|
Type: "alertmanager",
|
||||||
|
Settings: settingsJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
not, err := NewAlertmanagerNotifier(model)
|
||||||
|
alertmanagerNotifier := not.(*AlertmanagerNotifier)
|
||||||
|
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(alertmanagerNotifier.Url, ShouldEqual, "http://127.0.0.1:9093/")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user