Просмотр исходного кода

Merge remote-tracking branch 'upstream/master' into postgres-query-builder

Sven Klemm 7 лет назад
Родитель
Сommit
9e05477558
100 измененных файлов с 1281 добавлено и 683 удалено
  1. 18 0
      .dockerignore
  2. 2 0
      .gitignore
  3. 16 1
      CHANGELOG.md
  4. 13 1
      Gopkg.lock
  5. 2 0
      Makefile
  6. 10 16
      ROADMAP.md
  7. 1 1
      docker/blocks/apache_proxy/docker-compose.yaml
  8. 13 1
      docker/blocks/mssql/build/setup.sql.template
  9. 1 1
      docker/blocks/mssql/docker-compose.yaml
  10. 1 1
      docker/blocks/nginx_proxy/docker-compose.yaml
  11. 1 1
      docs/sources/administration/provisioning.md
  12. 3 3
      docs/sources/guides/whats-new-in-v4-1.md
  13. 1 1
      docs/sources/guides/whats-new-in-v4-2.md
  14. 1 1
      docs/sources/guides/whats-new-in-v4-6.md
  15. 3 3
      docs/sources/guides/whats-new-in-v5-1.md
  16. 1 0
      docs/sources/http_api/dashboard_permissions.md
  17. 2 0
      docs/sources/http_api/org.md
  18. 2 2
      docs/sources/installation/behind_proxy.md
  19. 5 3
      docs/sources/installation/configuration.md
  20. 3 3
      docs/sources/installation/debian.md
  21. 5 5
      docs/sources/installation/rpm.md
  22. 1 1
      docs/sources/installation/windows.md
  23. 1 1
      docs/sources/plugins/developing/apps.md
  24. 1 1
      docs/sources/plugins/developing/datasources.md
  25. 19 10
      docs/sources/plugins/developing/panels.md
  26. 1 1
      docs/sources/plugins/developing/plugin.json.md
  27. 2 0
      docs/sources/reference/dashboard.md
  28. 4 4
      docs/sources/tutorials/iis.md
  29. 1 1
      docs/sources/tutorials/screencasts.md
  30. 3 0
      package.json
  31. 19 10
      pkg/api/http_server.go
  32. 3 1
      pkg/cmd/grafana-cli/commands/commands.go
  33. 8 24
      pkg/cmd/grafana-server/main.go
  34. 57 53
      pkg/cmd/grafana-server/server.go
  35. 9 0
      pkg/components/null/float.go
  36. 1 1
      pkg/login/ldap.go
  37. 14 0
      pkg/login/ldap_test.go
  38. 5 1
      pkg/login/ldap_user.go
  39. 11 0
      pkg/middleware/auth_proxy.go
  40. 7 6
      pkg/models/notifications.go
  41. 9 0
      pkg/plugins/datasource/wrapper/datasource_plugin_wrapper.go
  42. 35 4
      pkg/registry/registry.go
  43. 12 12
      pkg/services/alerting/engine.go
  44. 173 0
      pkg/services/alerting/notifiers/discord.go
  45. 52 0
      pkg/services/alerting/notifiers/discord_test.go
  46. 7 6
      pkg/services/notifications/notifications.go
  47. 13 7
      pkg/services/notifications/webhook.go
  48. 1 1
      pkg/services/provisioning/dashboards/config_reader.go
  49. 3 3
      pkg/services/provisioning/dashboards/config_reader_test.go
  50. 2 5
      pkg/services/provisioning/dashboards/dashboard.go
  51. 4 4
      pkg/services/provisioning/dashboards/file_reader_test.go
  52. 0 0
      pkg/services/provisioning/dashboards/testdata/test-configs/broken-configs/commented.yaml
  53. 0 0
      pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml
  54. 0 0
      pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/sample.yaml
  55. 0 0
      pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml
  56. 0 0
      pkg/services/provisioning/dashboards/testdata/test-dashboards/broken-dashboards/empty-json.json
  57. 0 0
      pkg/services/provisioning/dashboards/testdata/test-dashboards/broken-dashboards/invalid.json
  58. 0 0
      pkg/services/provisioning/dashboards/testdata/test-dashboards/containing-id/dashboard1.json
  59. 0 0
      pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard1.json
  60. 0 0
      pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard2.json
  61. 0 0
      pkg/services/provisioning/dashboards/testdata/test-dashboards/one-dashboard/dashboard1.json
  62. 23 13
      pkg/services/provisioning/provisioning.go
  63. 1 1
      pkg/services/sqlstore/alert.go
  64. 0 2
      pkg/services/sqlstore/alert_notification_test.go
  65. 5 5
      pkg/services/sqlstore/annotation.go
  66. 16 3
      pkg/services/sqlstore/annotation_test.go
  67. 30 32
      pkg/services/sqlstore/dashboard_snapshot_test.go
  68. 2 8
      pkg/services/sqlstore/migrations/annotation_mig.go
  69. 1 4
      pkg/services/sqlstore/migrations/dashboard_acl.go
  70. 2 4
      pkg/services/sqlstore/migrations/dashboard_mig.go
  71. 1 3
      pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go
  72. 3 11
      pkg/services/sqlstore/migrations/dashboard_version_mig.go
  73. 1 4
      pkg/services/sqlstore/migrations/datasource_mig.go
  74. 2 2
      pkg/services/sqlstore/migrations/migrations_test.go
  75. 1 25
      pkg/services/sqlstore/migrations/org_mig.go
  76. 3 4
      pkg/services/sqlstore/migrations/user_auth_mig.go
  77. 2 41
      pkg/services/sqlstore/migrator/column.go
  78. 103 11
      pkg/services/sqlstore/migrator/dialect.go
  79. 38 17
      pkg/services/sqlstore/migrator/migrations.go
  80. 2 2
      pkg/services/sqlstore/migrator/migrator.go
  81. 25 5
      pkg/services/sqlstore/migrator/mysql_dialect.go
  82. 20 6
      pkg/services/sqlstore/migrator/postgres_dialect.go
  83. 11 6
      pkg/services/sqlstore/migrator/sqlite_dialect.go
  84. 1 0
      pkg/services/sqlstore/migrator/types.go
  85. 1 1
      pkg/services/sqlstore/playlist.go
  86. 1 0
      pkg/services/sqlstore/quota_test.go
  87. 3 5
      pkg/services/sqlstore/search_builder.go
  88. 2 5
      pkg/services/sqlstore/search_builder_test.go
  89. 21 0
      pkg/services/sqlstore/shared.go
  90. 167 144
      pkg/services/sqlstore/sqlstore.go
  91. 0 37
      pkg/services/sqlstore/sqlutil/sqlutil.go
  92. 1 2
      pkg/services/sqlstore/team.go
  93. 8 2
      pkg/services/sqlstore/user.go
  94. 22 9
      pkg/setting/setting.go
  95. 55 43
      pkg/tracing/tracing.go
  96. 17 2
      public/app/containers/Explore/Explore.tsx
  97. 15 4
      public/app/containers/Explore/QueryRows.tsx
  98. 1 0
      public/app/core/services/analytics.ts
  99. 13 1
      public/app/core/services/keybindingSrv.ts
  100. 79 18
      public/app/core/specs/file_export.jest.ts

+ 18 - 0
.dockerignore

@@ -0,0 +1,18 @@
+.awcache
+.dockerignore
+.git
+.gitignore
+.github
+data*
+dist
+docker
+docs
+dump.rdb
+node_modules
+/local
+/tmp
+/vendor
+*.yml
+*.md
+/vendor
+/tmp

+ 2 - 0
.gitignore

@@ -44,7 +44,9 @@ docker-compose.yaml
 /conf/provisioning/**/custom.yaml
 /conf/provisioning/**/custom.yaml
 profile.cov
 profile.cov
 /grafana
 /grafana
+/local
 .notouch
 .notouch
+/Makefile.local
 /pkg/cmd/grafana-cli/grafana-cli
 /pkg/cmd/grafana-cli/grafana-cli
 /pkg/cmd/grafana-server/grafana-server
 /pkg/cmd/grafana-server/grafana-server
 /pkg/cmd/grafana-server/debug
 /pkg/cmd/grafana-server/debug

+ 16 - 1
CHANGELOG.md

@@ -7,12 +7,27 @@
 * **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda)
 * **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda)
 * **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm)
 * **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm)
 * **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616)
 * **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616)
+* **Discord**: Alert notification channel type for Discord, [#7964](https://github.com/grafana/grafana/issues/7964) thx [@jereksel](https://github.com/jereksel),
+* **InfluxDB**: Support SELECT queries in templating query, [#5013](https://github.com/grafana/grafana/issues/5013)
+* **Dashboard**: JSON Model under dashboard settings can now be updated & changes saved, [#1429](https://github.com/grafana/grafana/issues/1429), thx [@jereksel](https://github.com/jereksel)
+* **Security**: Fix XSS vulnerabilities in dashboard links [#11813](https://github.com/grafana/grafana/pull/11813)
+* **Singlestat**: Fix "time of last point" shows local time when dashboard timezone set to UTC [#10338](https://github.com/grafana/grafana/issues/10338)
 
 
-# 5.1.1 (unreleased)
+# 5.1.3 (2018-05-16)
+
+* **Scroll**: Graph panel / legend texts shifts on the left each time we move scrollbar on firefox [#11830](https://github.com/grafana/grafana/issues/11830)
+
+# 5.1.2 (2018-05-09)
+
+* **Database**: Fix MySql migration issue [#11862](https://github.com/grafana/grafana/issues/11862)
+* **Google Analytics**: Enable Google Analytics anonymizeIP setting for GDPR [#11656](https://github.com/grafana/grafana/pull/11656)
+
+# 5.1.1 (2018-05-07)
 
 
 * **LDAP**: LDAP login with MariaDB/MySQL database and dn>100 chars not possible [#11754](https://github.com/grafana/grafana/issues/11754)
 * **LDAP**: LDAP login with MariaDB/MySQL database and dn>100 chars not possible [#11754](https://github.com/grafana/grafana/issues/11754)
 * **Build**: AppVeyor Windows build missing version and commit info [#11758](https://github.com/grafana/grafana/issues/11758)
 * **Build**: AppVeyor Windows build missing version and commit info [#11758](https://github.com/grafana/grafana/issues/11758)
 * **Scroll**: Scroll can't start in graphs on Chrome mobile [#11710](https://github.com/grafana/grafana/issues/11710)
 * **Scroll**: Scroll can't start in graphs on Chrome mobile [#11710](https://github.com/grafana/grafana/issues/11710)
+* **Units**: Revert renaming of unit key ppm [#11743](https://github.com/grafana/grafana/issues/11743)
 
 
 # 5.1.0 (2018-04-26)
 # 5.1.0 (2018-04-26)
 
 

+ 13 - 1
Gopkg.lock

@@ -111,6 +111,18 @@
   ]
   ]
   revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
   revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
 
 
+[[projects]]
+  branch = "master"
+  name = "github.com/facebookgo/inject"
+  packages = ["."]
+  revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/facebookgo/structtag"
+  packages = ["."]
+  revision = "217e25fb96916cc60332e399c9aa63f5c422ceed"
+
 [[projects]]
 [[projects]]
   name = "github.com/fatih/color"
   name = "github.com/fatih/color"
   packages = ["."]
   packages = ["."]
@@ -649,6 +661,6 @@
 [solve-meta]
 [solve-meta]
   analyzer-name = "dep"
   analyzer-name = "dep"
   analyzer-version = 1
   analyzer-version = 1
-  inputs-digest = "2bd5b309496d57e2189a1cc28f5c1c41398c19729ba0cf53c8cbb17ea3f706b5"
+  inputs-digest = "bd54a1a836599d90b36d4ac1af56d716ef9ca5be4865e217bddd49e3d32a1997"
   solver-name = "gps-cdcl"
   solver-name = "gps-cdcl"
   solver-version = 1
   solver-version = 1

+ 2 - 0
Makefile

@@ -1,3 +1,5 @@
+-include local/Makefile
+
 all: deps build
 all: deps build
 
 
 deps-go:
 deps-go:

+ 10 - 16
ROADMAP.md

@@ -1,26 +1,20 @@
-# Roadmap (2018-02-22)
+# Roadmap (2018-05-06)
 
 
 This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. 
 This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. 
 But it will give you an idea of our current vision and plan. 
 But it will give you an idea of our current vision and plan. 
-
-### Short term (1-2 months)
-
-- v5.1
-  - Build speed improvements & integration test execution
-  - Kubernetes friendly docker container
-  - Enterprise LDAP
-  - Provisioning workflow
-  - MSSQL datasource
   
   
-### Mid term (2-4 months)
+### Short term (1-2 months)
 
 
-- v5.2
-  - Azure monitor backend rewrite
   - Elasticsearch alerting
   - Elasticsearch alerting
-  - First login registration view
-  - Backend plugins? (alert notifiers, auth)
   - Crossplatform builds
   - Crossplatform builds
-  - IFQL Initial support
+  - Backend service refactorings
+  - Explore UI 
+  - First login registration view
+  
+### Mid term (2-4 months)
+  - Multi-Stat panel
+  - React Panels 
+  - Templating Query Editor UI Plugin hook
   
   
 ### Long term (4 - 8 months)
 ### Long term (4 - 8 months)
 
 

+ 1 - 1
docker/blocks/apache_proxy/docker-compose.yaml

@@ -2,7 +2,7 @@
 # http://localhost:3000 (Grafana running locally)
 # http://localhost:3000 (Grafana running locally)
 #
 #
 # Please note that you'll need to change the root_url in the Grafana configuration:
 # Please note that you'll need to change the root_url in the Grafana configuration:
-# root_url = %(protocol)s://%(domain)s:/grafana/
+# root_url = %(protocol)s://%(domain)s:10081/grafana/
 
 
   apacheproxy:
   apacheproxy:
     build: blocks/apache_proxy
     build: blocks/apache_proxy

+ 13 - 1
docker/blocks/mssql/build/setup.sql.template

@@ -1,7 +1,19 @@
 CREATE LOGIN %%USER%% WITH PASSWORD = '%%PWD%%'
 CREATE LOGIN %%USER%% WITH PASSWORD = '%%PWD%%'
 GO
 GO
 
 
-CREATE DATABASE %%DB%%;
+CREATE DATABASE %%DB%%
+ON
+( NAME = %%DB%%,
+    FILENAME = '/var/opt/mssql/data/%%DB%%.mdf',
+    SIZE = 500MB,
+    MAXSIZE = 1000MB,
+    FILEGROWTH = 100MB )
+LOG ON
+( NAME = %%DB%%_log,
+    FILENAME = '/var/opt/mssql/data/%%DB%%_log.ldf',
+    SIZE = 500MB,
+    MAXSIZE = 1000MB,
+    FILEGROWTH = 100MB );
 GO
 GO
 
 
 USE %%DB%%;
 USE %%DB%%;

+ 1 - 1
docker/blocks/mssql/docker-compose.yaml

@@ -4,7 +4,7 @@
     environment:
     environment:
       ACCEPT_EULA: Y
       ACCEPT_EULA: Y
       MSSQL_SA_PASSWORD: Password!
       MSSQL_SA_PASSWORD: Password!
-      MSSQL_PID: Express
+      MSSQL_PID: Developer
       MSSQL_DATABASE: grafana
       MSSQL_DATABASE: grafana
       MSSQL_USER: grafana
       MSSQL_USER: grafana
       MSSQL_PASSWORD: Password!
       MSSQL_PASSWORD: Password!

+ 1 - 1
docker/blocks/nginx_proxy/docker-compose.yaml

@@ -2,7 +2,7 @@
 # http://localhost:3000 (Grafana running locally)
 # http://localhost:3000 (Grafana running locally)
 #
 #
 # Please note that you'll need to change the root_url in the Grafana configuration:
 # Please note that you'll need to change the root_url in the Grafana configuration:
-# root_url = %(protocol)s://%(domain)s:/grafana/
+# root_url = %(protocol)s://%(domain)s:10080/grafana/
 
 
   nginxproxy:
   nginxproxy:
     build: blocks/nginx_proxy
     build: blocks/nginx_proxy

+ 1 - 1
docs/sources/administration/provisioning.md

@@ -94,7 +94,7 @@ deleteDatasources:
     orgId: 1
     orgId: 1
 
 
 # list of datasources to insert/update depending
 # list of datasources to insert/update depending
-# whats available in the database
+# what's available in the database
 datasources:
 datasources:
   # <string, required> name of the datasource. Required
   # <string, required> name of the datasource. Required
 - name: Graphite
 - name: Graphite

+ 3 - 3
docs/sources/guides/whats-new-in-v4-1.md

@@ -11,7 +11,7 @@ weight = 3
 +++
 +++
 
 
 
 
-## Whats new in Grafana v4.1
+## What's new in Grafana v4.1
 - **Graph**: Support for shared tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274)
 - **Graph**: Support for shared tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274)
 - **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411), thx [@ichekrygin](https://github.com/ichekrygin)
 - **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411), thx [@ichekrygin](https://github.com/ichekrygin)
 - **Opsgenie**: Add OpsGenie notification integratiion [#6687](https://github.com/grafana/grafana/issues/6687), thx [@kylemcc](https://github.com/kylemcc)
 - **Opsgenie**: Add OpsGenie notification integratiion [#6687](https://github.com/grafana/grafana/issues/6687), thx [@kylemcc](https://github.com/kylemcc)
@@ -24,7 +24,7 @@ weight = 3
 
 
 {{< imgbox max-width="60%" img="/img/docs/v41/shared_tooltip.gif" caption="Shared tooltip" >}}
 {{< imgbox max-width="60%" img="/img/docs/v41/shared_tooltip.gif" caption="Shared tooltip" >}}
 
 
-Showing the tooltip on all panels at the same time has been a long standing request in Grafana and we are really happy to finally be able to release it. 
+Showing the tooltip on all panels at the same time has been a long standing request in Grafana and we are really happy to finally be able to release it.
 You can enable/disable the shared tooltip from the dashboard settings menu or cycle between default, shared tooltip and shared crosshair by pressing `CTRL + O` or `CMD + O`.
 You can enable/disable the shared tooltip from the dashboard settings menu or cycle between default, shared tooltip and shared crosshair by pressing `CTRL + O` or `CMD + O`.
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
@@ -50,7 +50,7 @@ Panels with a help text available have a little indicator in the top left corner
 In Grafana 4.1.0 you can configure your Cloudwatch data source with `access key` and `secret key` directly in the data source configuration page.
 In Grafana 4.1.0 you can configure your Cloudwatch data source with `access key` and `secret key` directly in the data source configuration page.
 This enables people to use the Cloudwatch data source without having access to the filesystem where Grafana is running.
 This enables people to use the Cloudwatch data source without having access to the filesystem where Grafana is running.
 
 
-Once the `access key` and `secret key` have been saved the user will no longer be able to view them. 
+Once the `access key` and `secret key` have been saved the user will no longer be able to view them.
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 
 ## Upgrade & Breaking changes
 ## Upgrade & Breaking changes

+ 1 - 1
docs/sources/guides/whats-new-in-v4-2.md

@@ -10,7 +10,7 @@ parent = "whatsnew"
 weight = -1
 weight = -1
 +++
 +++
 
 
-## Whats new in Grafana v4.2
+## What's new in Grafana v4.2
 
 
 Grafana v4.2 Beta is now [available for download](https://grafana.com/grafana/download/4.2.0).
 Grafana v4.2 Beta is now [available for download](https://grafana.com/grafana/download/4.2.0).
 Just like the last release this one contains lots bug fixes and minor improvements.
 Just like the last release this one contains lots bug fixes and minor improvements.

+ 1 - 1
docs/sources/guides/whats-new-in-v4-6.md

@@ -64,7 +64,7 @@ This makes exploring and filtering Prometheus data much easier.
 * **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
 * **Dataproxy**: Allow grafan to renegotiate tls connection [#9250](https://github.com/grafana/grafana/issues/9250)
 * **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367)
 * **HTTP**: set net.Dialer.DualStack to true for all http clients [#9367](https://github.com/grafana/grafana/pull/9367)
 * **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739)
 * **Alerting**: Add diff and percent diff as series reducers [#9386](https://github.com/grafana/grafana/pull/9386), thx [@shanhuhai5739](https://github.com/shanhuhai5739)
-* **Slack**: Allow images to be uploaded to slack when Token is precent [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)
+* **Slack**: Allow images to be uploaded to slack when Token is present [#7175](https://github.com/grafana/grafana/issues/7175), thx [@xginn8](https://github.com/xginn8)
 * **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn)
 * **Opsgenie**: Use their latest API instead of old version [#9399](https://github.com/grafana/grafana/pull/9399), thx [@cglrkn](https://github.com/cglrkn)
 * **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123)
 * **Table**: Add support for displaying the timestamp with milliseconds [#9429](https://github.com/grafana/grafana/pull/9429), thx [@s1061123](https://github.com/s1061123)
 * **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo)
 * **Hipchat**: Add metrics, message and image to hipchat notifications [#9110](https://github.com/grafana/grafana/issues/9110), thx [@eloo](https://github.com/eloo)

+ 3 - 3
docs/sources/guides/whats-new-in-v5-1.md

@@ -100,8 +100,8 @@ In the table below you can see some examples and you can find all different opti
 Filter Option | Example | Raw | Interpolated | Description
 Filter Option | Example | Raw | Interpolated | Description
 ------------ | ------------- | ------------- | -------------  | -------------
 ------------ | ------------- | ------------- | -------------  | -------------
 `glob` | ${servers:glob} |  `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob
 `glob` | ${servers:glob} |  `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob
-`regex` | ${servers:regex} | `'test.', 'test2'` |  `(test\\.|test2)` | Formats multi-value variable into a regex string
-`pipe` | ${servers:pipe} | `'test.', 'test2'` |  `test.|test2` | Formats multi-value variable into a pipe-separated string
+`regex` | ${servers:regex} | `'test.', 'test2'` |  <code>(test\.&#124;test2)</code> | Formats multi-value variable into a regex string
+`pipe` | ${servers:pipe} | `'test.', 'test2'` |  <code>test.&#124;test2</code> | Formats multi-value variable into a pipe-separated string
 `csv`| ${servers:csv} |  `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
 `csv`| ${servers:csv} |  `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string
 
 
 ## Improved workflow for provisioned dashboards
 ## Improved workflow for provisioned dashboards
@@ -122,4 +122,4 @@ More information in the [Provisioning documentation](/features/datasources/prome
 ## Changelog
 ## Changelog
 
 
 Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list
 Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list
-of new features, changes, and bug fixes.
+of new features, changes, and bug fixes.

+ 1 - 0
docs/sources/http_api/dashboard_permissions.md

@@ -106,6 +106,7 @@ Accept: application/json
 Content-Type: application/json
 Content-Type: application/json
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
 
 
+{
   "items": [
   "items": [
     {
     {
       "role": "Viewer",
       "role": "Viewer",

+ 2 - 0
docs/sources/http_api/org.md

@@ -380,6 +380,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
   "role":"Viewer"
   "role":"Viewer"
 }
 }
 ```
 ```
+Note: The api will only work when you pass the admin name and password
+to the request http url, like http://admin:admin@localhost:3000/api/orgs/1/users
 
 
 **Example Response**:
 **Example Response**:
 
 

+ 2 - 2
docs/sources/installation/behind_proxy.md

@@ -53,7 +53,7 @@ server {
 ```bash
 ```bash
 [server]
 [server]
 domain = foo.bar
 domain = foo.bar
-root_url = %(protocol)s://%(domain)s:/grafana
+root_url = %(protocol)s://%(domain)s/grafana/
 ```
 ```
 
 
 #### Nginx configuration with sub path
 #### Nginx configuration with sub path
@@ -98,7 +98,7 @@ Given:
     ```bash
     ```bash
     [server]
     [server]
     domain = localhost:8080
     domain = localhost:8080
-    root_url = %(protocol)s://%(domain)s:/grafana
+    root_url = %(protocol)s://%(domain)s/grafana/
     ```
     ```
 
 
 Create an Inbound Rule for the parent website (localhost:8080 in this example) in IIS Manager with the following settings:
 Create an Inbound Rule for the parent website (localhost:8080 in this example) in IIS Manager with the following settings:

+ 5 - 3
docs/sources/installation/configuration.md

@@ -93,8 +93,6 @@ Directory where grafana will automatically scan and look for plugins
 
 
 ### provisioning
 ### provisioning
 
 
-> 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
 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]
@@ -659,6 +657,10 @@ Set to `true` to enable auto sign up of users who do not exist in Grafana DB. De
 
 
 Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header.
 Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header.
 
 
+### headers
+
+Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`.
+
 <hr>
 <hr>
 
 
 ## [session]
 ## [session]
@@ -713,7 +715,7 @@ Analytics ID here. By default this feature is disabled.
 
 
 ## [dashboards]
 ## [dashboards]
 
 
-### versions_to_keep (introduced in v5.0)
+### versions_to_keep
 
 
 Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1.
 Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1.
 
 

+ 3 - 3
docs/sources/installation/debian.md

@@ -15,7 +15,7 @@ weight = 1
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-Stable for Debian-based Linux | [grafana_5.1.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb)
+Stable for Debian-based Linux | [grafana_5.1.3_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.3_amd64.deb)
 <!--
 <!--
 Beta for Debian-based Linux | [grafana_5.1.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_amd64.deb)
 Beta for Debian-based Linux | [grafana_5.1.0-beta1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0-beta1_amd64.deb)
 -->
 -->
@@ -27,9 +27,9 @@ installation.
 
 
 
 
 ```bash
 ```bash
-wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb
+wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.3_amd64.deb
 sudo apt-get install -y adduser libfontconfig
 sudo apt-get install -y adduser libfontconfig
-sudo dpkg -i grafana_5.1.0_amd64.deb
+sudo dpkg -i grafana_5.1.3_amd64.deb
 ```
 ```
 
 
 <!-- ## Install Latest Beta
 <!-- ## Install Latest Beta

+ 5 - 5
docs/sources/installation/rpm.md

@@ -15,7 +15,7 @@ weight = 2
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm)
+Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.3 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3-1.x86_64.rpm)
 <!--
 <!--
 Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.x86_64.rpm)
 Latest Beta for CentOS / Fedora / OpenSuse / Redhat Linux | [5.1.0-beta1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-beta1.x86_64.rpm)
 -->
 -->
@@ -28,7 +28,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-5.1.0-1.x86_64.rpm
+$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3-1.x86_64.rpm
 ```
 ```
 
 
 <!-- ## Install Beta
 <!-- ## Install Beta
@@ -42,15 +42,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-5.1.0-1.x86_64.rpm
+$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3-1.x86_64.rpm
 $ sudo yum install initscripts fontconfig
 $ sudo yum install initscripts fontconfig
-$ sudo rpm -Uvh grafana-5.1.0-1.x86_64.rpm
+$ sudo rpm -Uvh grafana-5.1.3-1.x86_64.rpm
 ```
 ```
 
 
 #### On OpenSuse:
 #### On OpenSuse:
 
 
 ```bash
 ```bash
-$ sudo rpm -i --nodeps grafana-5.1.0-1.x86_64.rpm
+$ sudo rpm -i --nodeps grafana-5.1.3-1.x86_64.rpm
 ```
 ```
 
 
 ## Install via YUM Repository
 ## Install via YUM Repository

+ 1 - 1
docs/sources/installation/windows.md

@@ -12,7 +12,7 @@ weight = 3
 
 
 Description | Download
 Description | Download
 ------------ | -------------
 ------------ | -------------
-Latest stable package for Windows | [grafana-5.1.0.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0.windows-x64.zip)
+Latest stable package for Windows | [grafana-5.1.3.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.3.windows-x64.zip)
 
 
 <!--
 <!--
 Latest beta package for Windows | [grafana.5.1.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.windows-x64.zip)
 Latest beta package for Windows | [grafana.5.1.0-beta1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.0.0-beta5.windows-x64.zip)

+ 1 - 1
docs/sources/plugins/developing/apps.md

@@ -5,7 +5,7 @@ type = "docs"
 [menu.docs]
 [menu.docs]
 name = "Developing App Plugins"
 name = "Developing App Plugins"
 parent = "developing"
 parent = "developing"
-weight = 6
+weight = 4
 +++
 +++
 
 
 # Grafana Apps
 # Grafana Apps

+ 1 - 1
docs/sources/plugins/developing/datasources.md

@@ -5,7 +5,7 @@ type = "docs"
 [menu.docs]
 [menu.docs]
 name = "Developing Datasource Plugins"
 name = "Developing Datasource Plugins"
 parent = "developing"
 parent = "developing"
-weight = 6
+weight = 5
 +++
 +++
 
 
 # Datasources
 # Datasources

+ 19 - 10
docs/sources/plugins/developing/panels.md

@@ -1,16 +1,11 @@
----
-page_title: Plugin panel
-page_description: Panel plugins for Grafana
-page_keywords: grafana, plugins, documentation
----
-
-
 +++
 +++
-title = "Installing Plugins"
+title = "Developing Panel Plugins"
+keywords = ["grafana", "plugins", "panel", "documentation"]
 type = "docs"
 type = "docs"
 [menu.docs]
 [menu.docs]
+name = "Developing Panel Plugins"
 parent = "developing"
 parent = "developing"
-weight = 1
+weight = 4
 +++
 +++
 
 
 
 
@@ -20,7 +15,21 @@ Panels are the main building blocks of dashboards.
 
 
 ## Panel development
 ## Panel development
 
 
-Examples
+
+### Scrolling
+The grafana dashboard framework controls the panel height.  To enable a scrollbar within the panel the PanelCtrl needs to set the scrollable static variable:
+
+```javascript
+export class MyPanelCtrl extends PanelCtrl {
+  static scrollable = true;
+  ...
+```
+
+In this case, make sure the template has a single `<div>...</div>` root.  The plugin loader will modify that element adding a scrollbar.
+
+
+
+### Examples
 
 
 - [clock-panel](https://github.com/grafana/clock-panel)
 - [clock-panel](https://github.com/grafana/clock-panel)
 - [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts)
 - [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts)

+ 1 - 1
docs/sources/plugins/developing/plugin.json.md

@@ -5,7 +5,7 @@ type = "docs"
 [menu.docs]
 [menu.docs]
 name = "plugin.json Schema"
 name = "plugin.json Schema"
 parent = "developing"
 parent = "developing"
-weight = 6
+weight = 8
 +++
 +++
 
 
 # Plugin.json
 # Plugin.json

+ 2 - 0
docs/sources/reference/dashboard.md

@@ -50,6 +50,7 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
   "annotations": {
   "annotations": {
     "list": []
     "list": []
   },
   },
+  "refresh": "5s",
   "schemaVersion": 16,
   "schemaVersion": 16,
   "version": 0,
   "version": 0,
   "links": []
   "links": []
@@ -71,6 +72,7 @@ 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 |
+| **refresh** | auto-refresh interval
 | **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema |
 | **schemaVersion** | version of the JSON schema (integer), incremented each time a Grafana update brings changes to said schema |
 | **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
 | **version** | version of the dashboard (integer), incremented each time the dashboard is updated |
 | **panels** | panels array, see below for detail. |
 | **panels** | panels array, see below for detail. |

+ 4 - 4
docs/sources/tutorials/iis.md

@@ -16,7 +16,7 @@ Example:
 - Parent site: http://localhost:8080
 - Parent site: http://localhost:8080
 - Grafana: http://localhost:3000
 - Grafana: http://localhost:3000
 
 
-Grafana as a subpath: http://localhost:8080/grafana 
+Grafana as a subpath: http://localhost:8080/grafana
 
 
 ## Setup
 ## Setup
 
 
@@ -33,7 +33,7 @@ Given that the subpath should be `grafana` and the parent site is `localhost:808
  ```bash
  ```bash
 [server]
 [server]
 domain = localhost:8080
 domain = localhost:8080
-root_url = %(protocol)s://%(domain)s:/grafana
+root_url = %(protocol)s://%(domain)s/grafana/
 ```
 ```
 
 
 Restart the Grafana server after changing the config file.
 Restart the Grafana server after changing the config file.
@@ -74,11 +74,11 @@ When navigating to the grafana url (`http://localhost:8080/grafana` in the examp
 
 
 1. The `root_url` setting in the Grafana config file does not match the parent url with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files):
 1. The `root_url` setting in the Grafana config file does not match the parent url with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files):
 
 
-    `; root_url = %(protocol)s://%(domain)s:/grafana`
+    `; root_url = %(protocol)s://%(domain)s/grafana/`
 
 
 2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS:
 2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS:
 
 
-    `root_url = %(protocol)s://%(domain)s:/grafana`
+    `root_url = %(protocol)s://%(domain)s/grafana/`
 
 
     pattern in Inbound Rule: `wrongsubpath(/)?(.*)`
     pattern in Inbound Rule: `wrongsubpath(/)?(.*)`
 
 

+ 1 - 1
docs/sources/tutorials/screencasts.md

@@ -94,7 +94,7 @@ weight = 10
     </a>
     </a>
     <figcaption>
     <figcaption>
        <a href="https://youtu.be/FC13uhFRsVw?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" target="_blank" rel="noopener noreferrer">
        <a href="https://youtu.be/FC13uhFRsVw?list=PLDGkOdUX1Ujo3wHw9-z5Vo12YLqXRjzg2" target="_blank" rel="noopener noreferrer">
-       #3 Whats New In Grafana 2.0
+       #3 What's New In Grafana 2.0
        </a>
        </a>
     </figcaption>
     </figcaption>
   </figure>
   </figure>

+ 3 - 0
package.json

@@ -179,5 +179,8 @@
     "tether": "^1.4.0",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
     "tether-drop": "https://github.com/torkelo/drop/tarball/master",
     "tinycolor2": "^1.4.1"
     "tinycolor2": "^1.4.1"
+  },
+  "resolutions": {
+    "caniuse-db": "1.0.30000772"
   }
   }
 }
 }

+ 19 - 10
pkg/api/http_server.go

@@ -26,9 +26,14 @@ import (
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/plugins"
+	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
+func init() {
+	registry.RegisterService(&HTTPServer{})
+}
+
 type HTTPServer struct {
 type HTTPServer struct {
 	log           log.Logger
 	log           log.Logger
 	macaron       *macaron.Macaron
 	macaron       *macaron.Macaron
@@ -41,12 +46,14 @@ type HTTPServer struct {
 	Bus           bus.Bus       `inject:""`
 	Bus           bus.Bus       `inject:""`
 }
 }
 
 
-func (hs *HTTPServer) Init() {
+func (hs *HTTPServer) Init() error {
 	hs.log = log.New("http.server")
 	hs.log = log.New("http.server")
 	hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
 	hs.cache = gocache.New(5*time.Minute, 10*time.Minute)
+
+	return nil
 }
 }
 
 
-func (hs *HTTPServer) Start(ctx context.Context) error {
+func (hs *HTTPServer) Run(ctx context.Context) error {
 	var err error
 	var err error
 
 
 	hs.context = ctx
 	hs.context = ctx
@@ -57,17 +64,18 @@ func (hs *HTTPServer) Start(ctx context.Context) error {
 	hs.streamManager.Run(ctx)
 	hs.streamManager.Run(ctx)
 
 
 	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
 	listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort)
-	hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
+	hs.log.Info("HTTP Server Listen", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath)
 
 
 	hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
 	hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron}
 
 
 	// handle http shutdown on server context done
 	// handle http shutdown on server context done
 	go func() {
 	go func() {
 		<-ctx.Done()
 		<-ctx.Done()
+		// Hacky fix for race condition between ListenAndServe and Shutdown
+		time.Sleep(time.Millisecond * 100)
 		if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
 		if err := hs.httpSrv.Shutdown(context.Background()); err != nil {
 			hs.log.Error("Failed to shutdown server", "error", err)
 			hs.log.Error("Failed to shutdown server", "error", err)
 		}
 		}
-		hs.log.Info("Stopped HTTP Server")
 	}()
 	}()
 
 
 	switch setting.Protocol {
 	switch setting.Protocol {
@@ -106,12 +114,6 @@ func (hs *HTTPServer) Start(ctx context.Context) error {
 	return err
 	return err
 }
 }
 
 
-func (hs *HTTPServer) Shutdown(ctx context.Context) error {
-	err := hs.httpSrv.Shutdown(ctx)
-	hs.log.Info("Stopped HTTP server")
-	return err
-}
-
 func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
 func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error {
 	if certfile == "" {
 	if certfile == "" {
 		return fmt.Errorf("cert_file cannot be empty when using HTTPS")
 		return fmt.Errorf("cert_file cannot be empty when using HTTPS")
@@ -172,6 +174,7 @@ func (hs *HTTPServer) newMacaron() *macaron.Macaron {
 		hs.mapStatic(m, route.Directory, "", pluginRoute)
 		hs.mapStatic(m, route.Directory, "", pluginRoute)
 	}
 	}
 
 
+	hs.mapStatic(m, setting.StaticRootPath, "build", "public/build")
 	hs.mapStatic(m, setting.StaticRootPath, "", "public")
 	hs.mapStatic(m, setting.StaticRootPath, "", "public")
 	hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
 	hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
 
 
@@ -239,6 +242,12 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string,
 		c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
 		c.Resp.Header().Set("Cache-Control", "public, max-age=3600")
 	}
 	}
 
 
+	if prefix == "public/build" {
+		headers = func(c *macaron.Context) {
+			c.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
+		}
+	}
+
 	if setting.Env == setting.DEV {
 	if setting.Env == setting.DEV {
 		headers = func(c *macaron.Context) {
 		headers = func(c *macaron.Context) {
 			c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")
 			c.Resp.Header().Set("Cache-Control", "max-age=0, must-revalidate, no-cache")

+ 3 - 1
pkg/cmd/grafana-cli/commands/commands.go

@@ -22,7 +22,9 @@ func runDbCommand(command func(commandLine CommandLine) error) func(context *cli
 			Args:     flag.Args(),
 			Args:     flag.Args(),
 		})
 		})
 
 
-		sqlstore.NewEngine()
+		engine := &sqlstore.SqlStore{}
+		engine.Cfg = cfg
+		engine.Init()
 
 
 		if err := command(cmd); err != nil {
 		if err := command(cmd); err != nil {
 			logger.Errorf("\n%s: ", color.RedString("Error"))
 			logger.Errorf("\n%s: ", color.RedString("Error"))

+ 8 - 24
pkg/cmd/grafana-server/main.go

@@ -39,7 +39,6 @@ var enterprise string
 var configFile = flag.String("config", "", "path to config file")
 var configFile = flag.String("config", "", "path to config file")
 var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
 var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory")
 var pidFile = flag.String("pidfile", "", "path to pid file")
 var pidFile = flag.String("pidfile", "", "path to pid file")
-var exitChan = make(chan int)
 
 
 func main() {
 func main() {
 	v := flag.Bool("v", false, "prints current version and exits")
 	v := flag.Bool("v", false, "prints current version and exits")
@@ -81,29 +80,20 @@ func main() {
 	setting.Enterprise, _ = strconv.ParseBool(enterprise)
 	setting.Enterprise, _ = strconv.ParseBool(enterprise)
 
 
 	metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
 	metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
-	shutdownCompleted := make(chan int)
-	server := NewGrafanaServer()
 
 
-	go listenToSystemSignals(server, shutdownCompleted)
+	server := NewGrafanaServer()
 
 
-	go func() {
-		code := 0
-		if err := server.Start(); err != nil {
-			log.Error2("Startup failed", "error", err)
-			code = 1
-		}
+	go listenToSystemSignals(server)
 
 
-		exitChan <- code
-	}()
+	err := server.Run()
 
 
-	code := <-shutdownCompleted
-	log.Info2("Grafana shutdown completed.", "code", code)
+	trace.Stop()
 	log.Close()
 	log.Close()
-	os.Exit(code)
+
+	server.Exit(err)
 }
 }
 
 
-func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) {
-	var code int
+func listenToSystemSignals(server *GrafanaServerImpl) {
 	signalChan := make(chan os.Signal, 1)
 	signalChan := make(chan os.Signal, 1)
 	ignoreChan := make(chan os.Signal, 1)
 	ignoreChan := make(chan os.Signal, 1)
 
 
@@ -112,12 +102,6 @@ func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int
 
 
 	select {
 	select {
 	case sig := <-signalChan:
 	case sig := <-signalChan:
-		trace.Stop() // Stops trace if profiling has been enabled
-		server.Shutdown(0, fmt.Sprintf("system signal: %s", sig))
-		shutdownCompleted <- 0
-	case code = <-exitChan:
-		trace.Stop() // Stops trace if profiling has been enabled
-		server.Shutdown(code, "startup error")
-		shutdownCompleted <- code
+		server.Shutdown(fmt.Sprintf("System signal: %s", sig))
 	}
 	}
 }
 }

+ 57 - 53
pkg/cmd/grafana-server/server.go

@@ -8,7 +8,6 @@ import (
 	"net"
 	"net"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
-	"reflect"
 	"strconv"
 	"strconv"
 	"time"
 	"time"
 
 
@@ -16,19 +15,15 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/registry"
-	"github.com/grafana/grafana/pkg/services/dashboards"
-	"github.com/grafana/grafana/pkg/services/provisioning"
 
 
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/sync/errgroup"
 
 
 	"github.com/grafana/grafana/pkg/api"
 	"github.com/grafana/grafana/pkg/api"
 	"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/services/sqlstore"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 
 
 	"github.com/grafana/grafana/pkg/social"
 	"github.com/grafana/grafana/pkg/social"
-	"github.com/grafana/grafana/pkg/tracing"
 
 
 	// self registering services
 	// self registering services
 	_ "github.com/grafana/grafana/pkg/extensions"
 	_ "github.com/grafana/grafana/pkg/extensions"
@@ -37,7 +32,10 @@ import (
 	_ "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"
 	_ "github.com/grafana/grafana/pkg/services/notifications"
 	_ "github.com/grafana/grafana/pkg/services/notifications"
+	_ "github.com/grafana/grafana/pkg/services/provisioning"
 	_ "github.com/grafana/grafana/pkg/services/search"
 	_ "github.com/grafana/grafana/pkg/services/search"
+	_ "github.com/grafana/grafana/pkg/services/sqlstore"
+	_ "github.com/grafana/grafana/pkg/tracing"
 )
 )
 
 
 func NewGrafanaServer() *GrafanaServerImpl {
 func NewGrafanaServer() *GrafanaServerImpl {
@@ -54,50 +52,36 @@ func NewGrafanaServer() *GrafanaServerImpl {
 }
 }
 
 
 type GrafanaServerImpl struct {
 type GrafanaServerImpl struct {
-	context       context.Context
-	shutdownFn    context.CancelFunc
-	childRoutines *errgroup.Group
-	log           log.Logger
-	cfg           *setting.Cfg
+	context            context.Context
+	shutdownFn         context.CancelFunc
+	childRoutines      *errgroup.Group
+	log                log.Logger
+	cfg                *setting.Cfg
+	shutdownReason     string
+	shutdownInProgress bool
 
 
 	RouteRegister api.RouteRegister `inject:""`
 	RouteRegister api.RouteRegister `inject:""`
 	HttpServer    *api.HTTPServer   `inject:""`
 	HttpServer    *api.HTTPServer   `inject:""`
 }
 }
 
 
-func (g *GrafanaServerImpl) Start() error {
+func (g *GrafanaServerImpl) Run() error {
 	g.loadConfiguration()
 	g.loadConfiguration()
 	g.writePIDFile()
 	g.writePIDFile()
 
 
-	// initSql
-	sqlstore.NewEngine() // TODO: this should return an error
-	sqlstore.EnsureAdminUser()
-
 	login.Init()
 	login.Init()
 	social.NewOAuthService()
 	social.NewOAuthService()
 
 
-	if err := provisioning.Init(g.context, setting.HomePath, g.cfg.Raw); err != nil {
-		return fmt.Errorf("Failed to provision Grafana from config. error: %v", err)
-	}
-
-	tracingCloser, err := tracing.Init(g.cfg.Raw)
-	if err != nil {
-		return fmt.Errorf("Tracing settings is not valid. error: %v", err)
-	}
-	defer tracingCloser.Close()
-
 	serviceGraph := inject.Graph{}
 	serviceGraph := inject.Graph{}
 	serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
 	serviceGraph.Provide(&inject.Object{Value: bus.GetBus()})
 	serviceGraph.Provide(&inject.Object{Value: g.cfg})
 	serviceGraph.Provide(&inject.Object{Value: g.cfg})
-	serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()})
 	serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
 	serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)})
-	serviceGraph.Provide(&inject.Object{Value: api.HTTPServer{}})
 
 
 	// self registered services
 	// self registered services
 	services := registry.GetServices()
 	services := registry.GetServices()
 
 
 	// Add all services to dependency graph
 	// Add all services to dependency graph
 	for _, service := range services {
 	for _, service := range services {
-		serviceGraph.Provide(&inject.Object{Value: service})
+		serviceGraph.Provide(&inject.Object{Value: service.Instance})
 	}
 	}
 
 
 	serviceGraph.Provide(&inject.Object{Value: g})
 	serviceGraph.Provide(&inject.Object{Value: g})
@@ -109,37 +93,56 @@ func (g *GrafanaServerImpl) Start() error {
 
 
 	// Init & start services
 	// Init & start services
 	for _, service := range services {
 	for _, service := range services {
-		if registry.IsDisabled(service) {
+		if registry.IsDisabled(service.Instance) {
 			continue
 			continue
 		}
 		}
 
 
-		g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name())
+		g.log.Info("Initializing " + service.Name)
 
 
-		if err := service.Init(); err != nil {
-			return fmt.Errorf("Service init failed %v", err)
+		if err := service.Instance.Init(); err != nil {
+			return fmt.Errorf("Service init failed: %v", err)
 		}
 		}
 	}
 	}
 
 
 	// Start background services
 	// Start background services
-	for index := range services {
-		service, ok := services[index].(registry.BackgroundService)
+	for _, srv := range services {
+		// variable needed for accessing loop variable in function callback
+		descriptor := srv
+		service, ok := srv.Instance.(registry.BackgroundService)
 		if !ok {
 		if !ok {
 			continue
 			continue
 		}
 		}
 
 
-		if registry.IsDisabled(services[index]) {
+		if registry.IsDisabled(descriptor.Instance) {
 			continue
 			continue
 		}
 		}
 
 
 		g.childRoutines.Go(func() error {
 		g.childRoutines.Go(func() error {
+			// Skip starting new service when shutting down
+			// Can happen when service stop/return during startup
+			if g.shutdownInProgress {
+				return nil
+			}
+
 			err := service.Run(g.context)
 			err := service.Run(g.context)
-			g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err)
+
+			// If error is not canceled then the service crashed
+			if err != context.Canceled && err != nil {
+				g.log.Error("Stopped "+descriptor.Name, "reason", err)
+			} else {
+				g.log.Info("Stopped "+descriptor.Name, "reason", err)
+			}
+
+			// Mark that we are in shutdown mode
+			// So more services are not started
+			g.shutdownInProgress = true
 			return err
 			return err
 		})
 		})
 	}
 	}
 
 
 	sendSystemdNotification("READY=1")
 	sendSystemdNotification("READY=1")
-	return g.startHttpServer()
+
+	return g.childRoutines.Wait()
 }
 }
 
 
 func (g *GrafanaServerImpl) loadConfiguration() {
 func (g *GrafanaServerImpl) loadConfiguration() {
@@ -158,28 +161,29 @@ func (g *GrafanaServerImpl) loadConfiguration() {
 	g.cfg.LogConfigSources()
 	g.cfg.LogConfigSources()
 }
 }
 
 
-func (g *GrafanaServerImpl) startHttpServer() error {
-	g.HttpServer.Init()
-
-	err := g.HttpServer.Start(g.context)
-
-	if err != nil {
-		return fmt.Errorf("Fail to start server. error: %v", err)
-	}
-
-	return nil
-}
-
-func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
-	g.log.Info("Shutdown started", "code", code, "reason", reason)
+func (g *GrafanaServerImpl) Shutdown(reason string) {
+	g.log.Info("Shutdown started", "reason", reason)
+	g.shutdownReason = reason
+	g.shutdownInProgress = true
 
 
 	// call cancel func on root context
 	// call cancel func on root context
 	g.shutdownFn()
 	g.shutdownFn()
 
 
 	// wait for child routines
 	// wait for child routines
-	if err := g.childRoutines.Wait(); err != nil && err != context.Canceled {
-		g.log.Error("Server shutdown completed", "error", err)
+	g.childRoutines.Wait()
+}
+
+func (g *GrafanaServerImpl) Exit(reason error) {
+	// default exit code is 1
+	code := 1
+
+	if reason == context.Canceled && g.shutdownReason != "" {
+		reason = fmt.Errorf(g.shutdownReason)
+		code = 0
 	}
 	}
+
+	g.log.Error("Server shutdown", "reason", reason)
+	os.Exit(code)
 }
 }
 
 
 func (g *GrafanaServerImpl) writePIDFile() {
 func (g *GrafanaServerImpl) writePIDFile() {

+ 9 - 0
pkg/components/null/float.go

@@ -106,6 +106,15 @@ func (f Float) String() string {
 	return fmt.Sprintf("%1.3f", f.Float64)
 	return fmt.Sprintf("%1.3f", f.Float64)
 }
 }
 
 
+// FullString returns float as string in full precision
+func (f Float) FullString() string {
+	if !f.Valid {
+		return "null"
+	}
+
+	return fmt.Sprintf("%f", f.Float64)
+}
+
 // SetValid changes this Float's value and also sets it to be non-null.
 // SetValid changes this Float's value and also sets it to be non-null.
 func (f *Float) SetValid(n float64) {
 func (f *Float) SetValid(n float64) {
 	f.Float64 = n
 	f.Float64 = n

+ 1 - 1
pkg/login/ldap.go

@@ -349,7 +349,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) {
 
 
 func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
 func getLdapAttrN(name string, result *ldap.SearchResult, n int) string {
 	if name == "DN" {
 	if name == "DN" {
-		return result.Entries[0].DN
+		return result.Entries[n].DN
 	}
 	}
 	for _, attr := range result.Entries[n].Attributes {
 	for _, attr := range result.Entries[n].Attributes {
 		if attr.Name == name {
 		if attr.Name == name {

+ 14 - 0
pkg/login/ldap_test.go

@@ -53,6 +53,20 @@ func TestLdapAuther(t *testing.T) {
 			So(result, ShouldEqual, user1)
 			So(result, ShouldEqual, user1)
 		})
 		})
 
 
+		ldapAutherScenario("Given group match with different case", func(sc *scenarioContext) {
+			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
+				LdapGroups: []*LdapGroupToOrgRole{
+					{GroupDN: "cn=users", OrgRole: "Admin"},
+				},
+			})
+
+			sc.userQueryReturns(user1)
+
+			result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"CN=users"}})
+			So(err, ShouldBeNil)
+			So(result, ShouldEqual, user1)
+		})
+
 		ldapAutherScenario("Given no existing grafana user", func(sc *scenarioContext) {
 		ldapAutherScenario("Given no existing grafana user", func(sc *scenarioContext) {
 			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
 			ldapAuther := NewLdapAuthenticator(&LdapServerConf{
 				LdapGroups: []*LdapGroupToOrgRole{
 				LdapGroups: []*LdapGroupToOrgRole{

+ 5 - 1
pkg/login/ldap_user.go

@@ -1,5 +1,9 @@
 package login
 package login
 
 
+import (
+	"strings"
+)
+
 type LdapUserInfo struct {
 type LdapUserInfo struct {
 	DN        string
 	DN        string
 	FirstName string
 	FirstName string
@@ -15,7 +19,7 @@ func (u *LdapUserInfo) isMemberOf(group string) bool {
 	}
 	}
 
 
 	for _, member := range u.MemberOf {
 	for _, member := range u.MemberOf {
-		if member == group {
+		if strings.EqualFold(member, group) {
 			return true
 			return true
 		}
 		}
 	}
 	}

+ 11 - 0
pkg/middleware/auth_proxy.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"fmt"
 	"net"
 	"net"
 	"net/mail"
 	"net/mail"
+	"reflect"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -111,6 +112,16 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 			return true
 			return true
 		}
 		}
 
 
+		for _, field := range []string{"Name", "Email", "Login"} {
+			if setting.AuthProxyHeaders[field] == "" {
+				continue
+			}
+
+			if val := ctx.Req.Header.Get(setting.AuthProxyHeaders[field]); val != "" {
+				reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val)
+			}
+		}
+
 		// add/update user in grafana
 		// add/update user in grafana
 		cmd := &m.UpsertUserCommand{
 		cmd := &m.UpsertUserCommand{
 			ReqContext:    ctx,
 			ReqContext:    ctx,

+ 7 - 6
pkg/models/notifications.go

@@ -19,12 +19,13 @@ type SendEmailCommandSync struct {
 }
 }
 
 
 type SendWebhookSync struct {
 type SendWebhookSync struct {
-	Url        string
-	User       string
-	Password   string
-	Body       string
-	HttpMethod string
-	HttpHeader map[string]string
+	Url         string
+	User        string
+	Password    string
+	Body        string
+	HttpMethod  string
+	HttpHeader  map[string]string
+	ContentType string
 }
 }
 
 
 type SendResetPasswordEmailCommand struct {
 type SendResetPasswordEmailCommand struct {

+ 9 - 0
pkg/plugins/datasource/wrapper/datasource_plugin_wrapper.go

@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"fmt"
 
 
 	"github.com/grafana/grafana/pkg/components/null"
 	"github.com/grafana/grafana/pkg/components/null"
+	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
@@ -79,6 +80,14 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou
 			qr.ErrorString = r.Error
 			qr.ErrorString = r.Error
 		}
 		}
 
 
+		if r.MetaJson != "" {
+			metaJson, err := simplejson.NewJson([]byte(r.MetaJson))
+			if err != nil {
+				tw.logger.Error("Error parsing JSON Meta field: " + err.Error())
+			}
+			qr.Meta = metaJson
+		}
+
 		for _, s := range r.GetSeries() {
 		for _, s := range r.GetSeries() {
 			points := tsdb.TimeSeriesPoints{}
 			points := tsdb.TimeSeriesPoints{}
 
 

+ 35 - 4
pkg/registry/registry.go

@@ -2,15 +2,35 @@ package registry
 
 
 import (
 import (
 	"context"
 	"context"
+	"reflect"
+	"sort"
 )
 )
 
 
-var services = []Service{}
+type Descriptor struct {
+	Name         string
+	Instance     Service
+	InitPriority Priority
+}
+
+var services []*Descriptor
 
 
-func RegisterService(srv Service) {
-	services = append(services, srv)
+func RegisterService(instance Service) {
+	services = append(services, &Descriptor{
+		Name:         reflect.TypeOf(instance).Elem().Name(),
+		Instance:     instance,
+		InitPriority: Low,
+	})
 }
 }
 
 
-func GetServices() []Service {
+func Register(descriptor *Descriptor) {
+	services = append(services, descriptor)
+}
+
+func GetServices() []*Descriptor {
+	sort.Slice(services, func(i, j int) bool {
+		return services[i].InitPriority > services[j].InitPriority
+	})
+
 	return services
 	return services
 }
 }
 
 
@@ -27,7 +47,18 @@ type BackgroundService interface {
 	Run(ctx context.Context) error
 	Run(ctx context.Context) error
 }
 }
 
 
+type HasInitPriority interface {
+	GetInitPriority() Priority
+}
+
 func IsDisabled(srv Service) bool {
 func IsDisabled(srv Service) bool {
 	canBeDisabled, ok := srv.(CanBeDisabled)
 	canBeDisabled, ok := srv.(CanBeDisabled)
 	return ok && canBeDisabled.IsDisabled()
 	return ok && canBeDisabled.IsDisabled()
 }
 }
+
+type Priority int
+
+const (
+	High Priority = 100
+	Low  Priority = 0
+)

+ 12 - 12
pkg/services/alerting/engine.go

@@ -16,7 +16,7 @@ import (
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/sync/errgroup"
 )
 )
 
 
-type Engine struct {
+type AlertingService struct {
 	execQueue chan *Job
 	execQueue chan *Job
 	//clock         clock.Clock
 	//clock         clock.Clock
 	ticker        *Ticker
 	ticker        *Ticker
@@ -28,20 +28,20 @@ type Engine struct {
 }
 }
 
 
 func init() {
 func init() {
-	registry.RegisterService(&Engine{})
+	registry.RegisterService(&AlertingService{})
 }
 }
 
 
-func NewEngine() *Engine {
-	e := &Engine{}
+func NewEngine() *AlertingService {
+	e := &AlertingService{}
 	e.Init()
 	e.Init()
 	return e
 	return e
 }
 }
 
 
-func (e *Engine) IsDisabled() bool {
+func (e *AlertingService) IsDisabled() bool {
 	return !setting.AlertingEnabled || !setting.ExecuteAlerts
 	return !setting.AlertingEnabled || !setting.ExecuteAlerts
 }
 }
 
 
-func (e *Engine) Init() error {
+func (e *AlertingService) Init() error {
 	e.ticker = NewTicker(time.Now(), time.Second*0, clock.New())
 	e.ticker = NewTicker(time.Now(), time.Second*0, clock.New())
 	e.execQueue = make(chan *Job, 1000)
 	e.execQueue = make(chan *Job, 1000)
 	e.scheduler = NewScheduler()
 	e.scheduler = NewScheduler()
@@ -52,7 +52,7 @@ func (e *Engine) Init() error {
 	return nil
 	return nil
 }
 }
 
 
-func (e *Engine) Run(ctx context.Context) error {
+func (e *AlertingService) Run(ctx context.Context) error {
 	alertGroup, ctx := errgroup.WithContext(ctx)
 	alertGroup, ctx := errgroup.WithContext(ctx)
 	alertGroup.Go(func() error { return e.alertingTicker(ctx) })
 	alertGroup.Go(func() error { return e.alertingTicker(ctx) })
 	alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
 	alertGroup.Go(func() error { return e.runJobDispatcher(ctx) })
@@ -61,7 +61,7 @@ func (e *Engine) Run(ctx context.Context) error {
 	return err
 	return err
 }
 }
 
 
-func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
+func (e *AlertingService) alertingTicker(grafanaCtx context.Context) error {
 	defer func() {
 	defer func() {
 		if err := recover(); err != nil {
 		if err := recover(); err != nil {
 			e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
 			e.log.Error("Scheduler Panic: stopping alertingTicker", "error", err, "stack", log.Stack(1))
@@ -86,7 +86,7 @@ func (e *Engine) alertingTicker(grafanaCtx context.Context) error {
 	}
 	}
 }
 }
 
 
-func (e *Engine) runJobDispatcher(grafanaCtx context.Context) error {
+func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error {
 	dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
 	dispatcherGroup, alertCtx := errgroup.WithContext(grafanaCtx)
 
 
 	for {
 	for {
@@ -106,7 +106,7 @@ var (
 	alertMaxAttempts = 3
 	alertMaxAttempts = 3
 )
 )
 
 
-func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
+func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
 	defer func() {
 	defer func() {
 		if err := recover(); err != nil {
 		if err := recover(); err != nil {
 			e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
 			e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
@@ -141,7 +141,7 @@ func (e *Engine) processJobWithRetry(grafanaCtx context.Context, job *Job) error
 	}
 	}
 }
 }
 
 
-func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
+func (e *AlertingService) endJob(err error, cancelChan chan context.CancelFunc, job *Job) error {
 	job.Running = false
 	job.Running = false
 	close(cancelChan)
 	close(cancelChan)
 	for cancelFn := range cancelChan {
 	for cancelFn := range cancelChan {
@@ -150,7 +150,7 @@ func (e *Engine) endJob(err error, cancelChan chan context.CancelFunc, job *Job)
 	return err
 	return err
 }
 }
 
 
-func (e *Engine) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
+func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancelChan chan context.CancelFunc, job *Job) {
 	defer func() {
 	defer func() {
 		if err := recover(); err != nil {
 		if err := recover(); err != nil {
 			e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))
 			e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1))

+ 173 - 0
pkg/services/alerting/notifiers/discord.go

@@ -0,0 +1,173 @@
+package notifiers
+
+import (
+	"bytes"
+	"io"
+	"mime/multipart"
+	"os"
+	"strconv"
+	"strings"
+
+	"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"
+	"github.com/grafana/grafana/pkg/setting"
+)
+
+func init() {
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "discord",
+		Name:        "Discord",
+		Description: "Sends notifications to Discord",
+		Factory:     NewDiscordNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">Discord settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-14">Webhook URL</span>
+        <input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.url" placeholder="Discord webhook URL"></input>
+      </div>
+    `,
+	})
+}
+
+func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+	url := model.Settings.Get("url").MustString()
+	if url == "" {
+		return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"}
+	}
+
+	return &DiscordNotifier{
+		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+		WebhookURL:   url,
+		log:          log.New("alerting.notifier.discord"),
+	}, nil
+}
+
+type DiscordNotifier struct {
+	NotifierBase
+	WebhookURL string
+	log        log.Logger
+}
+
+func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
+	this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL)
+
+	ruleUrl, err := evalContext.GetRuleUrl()
+	if err != nil {
+		this.log.Error("Failed get rule link", "error", err)
+		return err
+	}
+
+	bodyJSON := simplejson.New()
+	bodyJSON.Set("username", "Grafana")
+
+	fields := make([]map[string]interface{}, 0)
+
+	for _, evt := range evalContext.EvalMatches {
+
+		fields = append(fields, map[string]interface{}{
+			"name":   evt.Metric,
+			"value":  evt.Value.FullString(),
+			"inline": true,
+		})
+	}
+
+	footer := map[string]interface{}{
+		"text":     "Grafana v" + setting.BuildVersion,
+		"icon_url": "https://grafana.com/assets/img/fav32.png",
+	}
+
+	color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0)
+
+	embed := simplejson.New()
+	embed.Set("title", evalContext.GetNotificationTitle())
+	//Discord takes integer for color
+	embed.Set("color", color)
+	embed.Set("url", ruleUrl)
+	embed.Set("description", evalContext.Rule.Message)
+	embed.Set("type", "rich")
+	embed.Set("fields", fields)
+	embed.Set("footer", footer)
+
+	var image map[string]interface{}
+	var embeddedImage = false
+
+	if evalContext.ImagePublicUrl != "" {
+		image = map[string]interface{}{
+			"url": evalContext.ImagePublicUrl,
+		}
+		embed.Set("image", image)
+	} else {
+		image = map[string]interface{}{
+			"url": "attachment://graph.png",
+		}
+		embed.Set("image", image)
+		embeddedImage = true
+	}
+
+	bodyJSON.Set("embeds", []interface{}{embed})
+
+	json, _ := bodyJSON.MarshalJSON()
+
+	content_type := "application/json"
+
+	var body []byte
+
+	if embeddedImage {
+
+		var b bytes.Buffer
+
+		w := multipart.NewWriter(&b)
+
+		f, err := os.Open(evalContext.ImageOnDiskPath)
+
+		if err != nil {
+			this.log.Error("Can't open graph file", err)
+			return err
+		}
+
+		defer f.Close()
+
+		fw, err := w.CreateFormField("payload_json")
+		if err != nil {
+			return err
+		}
+
+		if _, err = fw.Write([]byte(string(json))); err != nil {
+			return err
+		}
+
+		fw, err = w.CreateFormFile("file", "graph.png")
+		if err != nil {
+			return err
+		}
+
+		if _, err = io.Copy(fw, f); err != nil {
+			return err
+		}
+
+		w.Close()
+
+		body = b.Bytes()
+		content_type = w.FormDataContentType()
+
+	} else {
+		body = json
+	}
+
+	cmd := &m.SendWebhookSync{
+		Url:         this.WebhookURL,
+		Body:        string(body),
+		HttpMethod:  "POST",
+		ContentType: content_type,
+	}
+
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send notification to Discord", "error", err)
+		return err
+	}
+
+	return nil
+}

+ 52 - 0
pkg/services/alerting/notifiers/discord_test.go

@@ -0,0 +1,52 @@
+package notifiers
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	m "github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestDiscordNotifier(t *testing.T) {
+	Convey("Telegram 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:     "discord_testing",
+					Type:     "discord",
+					Settings: settingsJSON,
+				}
+
+				_, err := NewDiscordNotifier(model)
+				So(err, ShouldNotBeNil)
+			})
+
+			Convey("settings should trigger incident", func() {
+				json := `
+				{
+          "url": "https://web.hook/"
+				}`
+
+				settingsJSON, _ := simplejson.NewJson([]byte(json))
+				model := &m.AlertNotification{
+					Name:     "discord_testing",
+					Type:     "discord",
+					Settings: settingsJSON,
+				}
+
+				not, err := NewDiscordNotifier(model)
+				discordNotifier := not.(*DiscordNotifier)
+
+				So(err, ShouldBeNil)
+				So(discordNotifier.Name, ShouldEqual, "discord_testing")
+				So(discordNotifier.Type, ShouldEqual, "discord")
+				So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/")
+			})
+		})
+	})
+}

+ 7 - 6
pkg/services/notifications/notifications.go

@@ -104,12 +104,13 @@ func (ns *NotificationService) Run(ctx context.Context) error {
 
 
 func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
 func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
 	return ns.sendWebRequestSync(ctx, &Webhook{
 	return ns.sendWebRequestSync(ctx, &Webhook{
-		Url:        cmd.Url,
-		User:       cmd.User,
-		Password:   cmd.Password,
-		Body:       cmd.Body,
-		HttpMethod: cmd.HttpMethod,
-		HttpHeader: cmd.HttpHeader,
+		Url:         cmd.Url,
+		User:        cmd.User,
+		Password:    cmd.Password,
+		Body:        cmd.Body,
+		HttpMethod:  cmd.HttpMethod,
+		HttpHeader:  cmd.HttpHeader,
+		ContentType: cmd.ContentType,
 	})
 	})
 }
 }
 
 

+ 13 - 7
pkg/services/notifications/webhook.go

@@ -15,12 +15,13 @@ import (
 )
 )
 
 
 type Webhook struct {
 type Webhook struct {
-	Url        string
-	User       string
-	Password   string
-	Body       string
-	HttpMethod string
-	HttpHeader map[string]string
+	Url         string
+	User        string
+	Password    string
+	Body        string
+	HttpMethod  string
+	HttpHeader  map[string]string
+	ContentType string
 }
 }
 
 
 var netTransport = &http.Transport{
 var netTransport = &http.Transport{
@@ -48,8 +49,13 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
 		return err
 		return err
 	}
 	}
 
 
-	request.Header.Add("Content-Type", "application/json")
+	if webhook.ContentType == "" {
+		webhook.ContentType = "application/json"
+	}
+
+	request.Header.Add("Content-Type", webhook.ContentType)
 	request.Header.Add("User-Agent", "Grafana")
 	request.Header.Add("User-Agent", "Grafana")
+
 	if webhook.User != "" && webhook.Password != "" {
 	if webhook.User != "" && webhook.Password != "" {
 		request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
 		request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
 	}
 	}

+ 1 - 1
pkg/services/provisioning/dashboards/config_reader.go

@@ -69,7 +69,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
 
 
 		parsedDashboards, err := cr.parseConfigs(file)
 		parsedDashboards, err := cr.parseConfigs(file)
 		if err != nil {
 		if err != nil {
-
+			return nil, err
 		}
 		}
 
 
 		if len(parsedDashboards) > 0 {
 		if len(parsedDashboards) > 0 {

+ 3 - 3
pkg/services/provisioning/dashboards/config_reader_test.go

@@ -8,9 +8,9 @@ import (
 )
 )
 
 
 var (
 var (
-	simpleDashboardConfig = "./test-configs/dashboards-from-disk"
-	oldVersion            = "./test-configs/version-0"
-	brokenConfigs         = "./test-configs/broken-configs"
+	simpleDashboardConfig = "./testdata/test-configs/dashboards-from-disk"
+	oldVersion            = "./testdata/test-configs/version-0"
+	brokenConfigs         = "./testdata/test-configs/broken-configs"
 )
 )
 
 
 func TestDashboardsAsConfig(t *testing.T) {
 func TestDashboardsAsConfig(t *testing.T) {

+ 2 - 5
pkg/services/provisioning/dashboards/dashboard.go

@@ -10,19 +10,16 @@ import (
 type DashboardProvisioner struct {
 type DashboardProvisioner struct {
 	cfgReader *configReader
 	cfgReader *configReader
 	log       log.Logger
 	log       log.Logger
-	ctx       context.Context
 }
 }
 
 
-func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) {
+func NewDashboardProvisioner(configDirectory string) *DashboardProvisioner {
 	log := log.New("provisioning.dashboard")
 	log := log.New("provisioning.dashboard")
 	d := &DashboardProvisioner{
 	d := &DashboardProvisioner{
 		cfgReader: &configReader{path: configDirectory, log: log},
 		cfgReader: &configReader{path: configDirectory, log: log},
 		log:       log,
 		log:       log,
-		ctx:       ctx,
 	}
 	}
 
 
-	err := d.Provision(ctx)
-	return d, err
+	return d
 }
 }
 
 
 func (provider *DashboardProvisioner) Provision(ctx context.Context) error {
 func (provider *DashboardProvisioner) Provision(ctx context.Context) error {

+ 4 - 4
pkg/services/provisioning/dashboards/file_reader_test.go

@@ -15,10 +15,10 @@ import (
 )
 )
 
 
 var (
 var (
-	defaultDashboards = "./test-dashboards/folder-one"
-	brokenDashboards  = "./test-dashboards/broken-dashboards"
-	oneDashboard      = "./test-dashboards/one-dashboard"
-	containingId      = "./test-dashboards/containing-id"
+	defaultDashboards = "./testdata/test-dashboards/folder-one"
+	brokenDashboards  = "./testdata/test-dashboards/broken-dashboards"
+	oneDashboard      = "./testdata/test-dashboards/one-dashboard"
+	containingId      = "./testdata/test-dashboards/containing-id"
 
 
 	fakeService *fakeDashboardProvisioningService
 	fakeService *fakeDashboardProvisioningService
 )
 )

+ 0 - 0
pkg/services/provisioning/dashboards/test-configs/broken-configs/commented.yaml → pkg/services/provisioning/dashboards/testdata/test-configs/broken-configs/commented.yaml


+ 0 - 0
pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml → pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml


+ 0 - 0
pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/sample.yaml → pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/sample.yaml


+ 0 - 0
pkg/services/provisioning/dashboards/test-configs/version-0/version-0.yaml → pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml


+ 0 - 0
pkg/services/provisioning/dashboards/test-dashboards/broken-dashboards/empty-json.json → pkg/services/provisioning/dashboards/testdata/test-dashboards/broken-dashboards/empty-json.json


+ 0 - 0
pkg/services/provisioning/dashboards/test-dashboards/broken-dashboards/invalid.json → pkg/services/provisioning/dashboards/testdata/test-dashboards/broken-dashboards/invalid.json


+ 0 - 0
pkg/services/provisioning/dashboards/test-dashboards/containing-id/dashboard1.json → pkg/services/provisioning/dashboards/testdata/test-dashboards/containing-id/dashboard1.json


+ 0 - 0
pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard1.json → pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard1.json


+ 0 - 0
pkg/services/provisioning/dashboards/test-dashboards/folder-one/dashboard2.json → pkg/services/provisioning/dashboards/testdata/test-dashboards/folder-one/dashboard2.json


+ 0 - 0
pkg/services/provisioning/dashboards/test-dashboards/one-dashboard/dashboard1.json → pkg/services/provisioning/dashboards/testdata/test-dashboards/one-dashboard/dashboard1.json


+ 23 - 13
pkg/services/provisioning/provisioning.go

@@ -2,30 +2,40 @@ package provisioning
 
 
 import (
 import (
 	"context"
 	"context"
+	"fmt"
 	"path"
 	"path"
-	"path/filepath"
 
 
+	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
 	"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
 	"github.com/grafana/grafana/pkg/services/provisioning/datasources"
 	"github.com/grafana/grafana/pkg/services/provisioning/datasources"
-	ini "gopkg.in/ini.v1"
+	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
-func Init(ctx context.Context, homePath string, cfg *ini.File) error {
-	provisioningPath := makeAbsolute(cfg.Section("paths").Key("provisioning").String(), homePath)
+func init() {
+	registry.RegisterService(&ProvisioningService{})
+}
+
+type ProvisioningService struct {
+	Cfg *setting.Cfg `inject:""`
+}
 
 
-	datasourcePath := path.Join(provisioningPath, "datasources")
+func (ps *ProvisioningService) Init() error {
+	datasourcePath := path.Join(ps.Cfg.ProvisioningPath, "datasources")
 	if err := datasources.Provision(datasourcePath); err != nil {
 	if err := datasources.Provision(datasourcePath); err != nil {
-		return err
+		return fmt.Errorf("Datasource provisioning error: %v", err)
 	}
 	}
 
 
-	dashboardPath := path.Join(provisioningPath, "dashboards")
-	_, err := dashboards.Provision(ctx, dashboardPath)
-	return err
+	return nil
 }
 }
 
 
-func makeAbsolute(path string, root string) string {
-	if filepath.IsAbs(path) {
-		return path
+func (ps *ProvisioningService) Run(ctx context.Context) error {
+	dashboardPath := path.Join(ps.Cfg.ProvisioningPath, "dashboards")
+	dashProvisioner := dashboards.NewDashboardProvisioner(dashboardPath)
+
+	if err := dashProvisioner.Provision(ctx); err != nil {
+		return err
 	}
 	}
-	return filepath.Join(root, path)
+
+	<-ctx.Done()
+	return ctx.Err()
 }
 }

+ 1 - 1
pkg/services/sqlstore/alert.go

@@ -114,7 +114,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 	builder.Write(" ORDER BY name ASC")
 	builder.Write(" ORDER BY name ASC")
 
 
 	if query.Limit != 0 {
 	if query.Limit != 0 {
-		builder.Write(" LIMIT ?", query.Limit)
+		builder.Write(dialect.Limit(query.Limit))
 	}
 	}
 
 
 	alerts := make([]*m.AlertListItemDTO, 0)
 	alerts := make([]*m.AlertListItemDTO, 0)

+ 0 - 2
pkg/services/sqlstore/alert_notification_test.go

@@ -1,7 +1,6 @@
 package sqlstore
 package sqlstore
 
 
 import (
 import (
-	"fmt"
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -21,7 +20,6 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
 			}
 			}
 
 
 			err := GetAlertNotifications(cmd)
 			err := GetAlertNotifications(cmd)
-			fmt.Printf("error %v", err)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(cmd.Result, ShouldBeNil)
 			So(cmd.Result, ShouldBeNil)
 		})
 		})

+ 5 - 5
pkg/services/sqlstore/annotation.go

@@ -50,7 +50,7 @@ func (r *SqlAnnotationRepo) ensureTagsExist(sess *DBSession, tags []*models.Tag)
 		var existingTag models.Tag
 		var existingTag models.Tag
 
 
 		// check if it exists
 		// check if it exists
-		if exists, err := sess.Table("tag").Where("`key`=? AND `value`=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
+		if exists, err := sess.Table("tag").Where(dialect.Quote("key")+"=? AND "+dialect.Quote("value")+"=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
 			return nil, err
 			return nil, err
 		} else if exists {
 		} else if exists {
 			tag.Id = existingTag.Id
 			tag.Id = existingTag.Id
@@ -146,7 +146,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
 	params = append(params, query.OrgId)
 	params = append(params, query.OrgId)
 
 
 	if query.AnnotationId != 0 {
 	if query.AnnotationId != 0 {
-		fmt.Print("annotation query")
+		// fmt.Print("annotation query")
 		sql.WriteString(` AND annotation.id = ?`)
 		sql.WriteString(` AND annotation.id = ?`)
 		params = append(params, query.AnnotationId)
 		params = append(params, query.AnnotationId)
 	}
 	}
@@ -193,10 +193,10 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
 		tags := models.ParseTagPairs(query.Tags)
 		tags := models.ParseTagPairs(query.Tags)
 		for _, tag := range tags {
 		for _, tag := range tags {
 			if tag.Value == "" {
 			if tag.Value == "" {
-				keyValueFilters = append(keyValueFilters, "(tag.key = ?)")
+				keyValueFilters = append(keyValueFilters, "(tag."+dialect.Quote("key")+" = ?)")
 				params = append(params, tag.Key)
 				params = append(params, tag.Key)
 			} else {
 			} else {
-				keyValueFilters = append(keyValueFilters, "(tag.key = ? AND tag.value = ?)")
+				keyValueFilters = append(keyValueFilters, "(tag."+dialect.Quote("key")+" = ? AND tag."+dialect.Quote("value")+" = ?)")
 				params = append(params, tag.Key, tag.Value)
 				params = append(params, tag.Key, tag.Value)
 			}
 			}
 		}
 		}
@@ -219,7 +219,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
 		query.Limit = 100
 		query.Limit = 100
 	}
 	}
 
 
-	sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
+	sql.WriteString(" ORDER BY epoch DESC" + dialect.Limit(query.Limit))
 
 
 	items := make([]*annotations.ItemDTO, 0)
 	items := make([]*annotations.ItemDTO, 0)
 
 

+ 16 - 3
pkg/services/sqlstore/annotation_test.go

@@ -10,12 +10,18 @@ import (
 )
 )
 
 
 func TestSavingTags(t *testing.T) {
 func TestSavingTags(t *testing.T) {
+	InitTestDB(t)
+
 	Convey("Testing annotation saving/loading", t, func() {
 	Convey("Testing annotation saving/loading", t, func() {
-		InitTestDB(t)
 
 
 		repo := SqlAnnotationRepo{}
 		repo := SqlAnnotationRepo{}
 
 
 		Convey("Can save tags", func() {
 		Convey("Can save tags", func() {
+			Reset(func() {
+				_, err := x.Exec("DELETE FROM annotation_tag WHERE 1=1")
+				So(err, ShouldBeNil)
+			})
+
 			tagPairs := []*models.Tag{
 			tagPairs := []*models.Tag{
 				{Key: "outage"},
 				{Key: "outage"},
 				{Key: "type", Value: "outage"},
 				{Key: "type", Value: "outage"},
@@ -31,12 +37,19 @@ func TestSavingTags(t *testing.T) {
 }
 }
 
 
 func TestAnnotations(t *testing.T) {
 func TestAnnotations(t *testing.T) {
-	Convey("Testing annotation saving/loading", t, func() {
-		InitTestDB(t)
+	InitTestDB(t)
 
 
+	Convey("Testing annotation saving/loading", t, func() {
 		repo := SqlAnnotationRepo{}
 		repo := SqlAnnotationRepo{}
 
 
 		Convey("Can save annotation", func() {
 		Convey("Can save annotation", func() {
+			Reset(func() {
+				_, err := x.Exec("DELETE FROM annotation WHERE 1=1")
+				So(err, ShouldBeNil)
+				_, err = x.Exec("DELETE FROM annotation_tag WHERE 1=1")
+				So(err, ShouldBeNil)
+			})
+
 			annotation := &annotations.Item{
 			annotation := &annotations.Item{
 				OrgId:       1,
 				OrgId:       1,
 				UserId:      1,
 				UserId:      1,

+ 30 - 32
pkg/services/sqlstore/dashboard_snapshot_test.go

@@ -4,7 +4,6 @@ import (
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
-	"github.com/go-xorm/xorm"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 
 
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -110,46 +109,43 @@ func TestDashboardSnapshotDBAccess(t *testing.T) {
 }
 }
 
 
 func TestDeleteExpiredSnapshots(t *testing.T) {
 func TestDeleteExpiredSnapshots(t *testing.T) {
-	Convey("Testing dashboard snapshots clean up", t, func() {
-		x := InitTestDB(t)
+	sqlstore := InitTestDB(t)
 
 
+	Convey("Testing dashboard snapshots clean up", t, func() {
 		setting.SnapShotRemoveExpired = true
 		setting.SnapShotRemoveExpired = true
 
 
-		notExpiredsnapshot := createTestSnapshot(x, "key1", 1000)
-		createTestSnapshot(x, "key2", -1000)
-		createTestSnapshot(x, "key3", -1000)
+		notExpiredsnapshot := createTestSnapshot(sqlstore, "key1", 48000)
+		createTestSnapshot(sqlstore, "key2", -1200)
+		createTestSnapshot(sqlstore, "key3", -1200)
 
 
-		Convey("Clean up old dashboard snapshots", func() {
-			err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
-			So(err, ShouldBeNil)
+		err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
+		So(err, ShouldBeNil)
 
 
-			query := m.GetDashboardSnapshotsQuery{
-				OrgId:        1,
-				SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
-			}
-			err = SearchDashboardSnapshots(&query)
-			So(err, ShouldBeNil)
+		query := m.GetDashboardSnapshotsQuery{
+			OrgId:        1,
+			SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
+		}
+		err = SearchDashboardSnapshots(&query)
+		So(err, ShouldBeNil)
 
 
-			So(len(query.Result), ShouldEqual, 1)
-			So(query.Result[0].Key, ShouldEqual, notExpiredsnapshot.Key)
-		})
+		So(len(query.Result), ShouldEqual, 1)
+		So(query.Result[0].Key, ShouldEqual, notExpiredsnapshot.Key)
 
 
-		Convey("Don't delete anything if there are no expired snapshots", func() {
-			err := DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
-			So(err, ShouldBeNil)
+		err = DeleteExpiredSnapshots(&m.DeleteExpiredSnapshotsCommand{})
+		So(err, ShouldBeNil)
 
 
-			query := m.GetDashboardSnapshotsQuery{
-				OrgId:        1,
-				SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
-			}
-			SearchDashboardSnapshots(&query)
+		query = m.GetDashboardSnapshotsQuery{
+			OrgId:        1,
+			SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_ADMIN},
+		}
+		SearchDashboardSnapshots(&query)
 
 
-			So(len(query.Result), ShouldEqual, 1)
-		})
+		So(len(query.Result), ShouldEqual, 1)
+		So(query.Result[0].Key, ShouldEqual, notExpiredsnapshot.Key)
 	})
 	})
 }
 }
 
 
-func createTestSnapshot(x *xorm.Engine, key string, expires int64) *m.DashboardSnapshot {
+func createTestSnapshot(sqlstore *SqlStore, key string, expires int64) *m.DashboardSnapshot {
 	cmd := m.CreateDashboardSnapshotCommand{
 	cmd := m.CreateDashboardSnapshotCommand{
 		Key:       key,
 		Key:       key,
 		DeleteKey: "delete" + key,
 		DeleteKey: "delete" + key,
@@ -164,9 +160,11 @@ func createTestSnapshot(x *xorm.Engine, key string, expires int64) *m.DashboardS
 	So(err, ShouldBeNil)
 	So(err, ShouldBeNil)
 
 
 	// Set expiry date manually - to be able to create expired snapshots
 	// Set expiry date manually - to be able to create expired snapshots
-	expireDate := time.Now().Add(time.Second * time.Duration(expires))
-	_, err = x.Exec("update dashboard_snapshot set expires = ? where "+dialect.Quote("key")+" = ?", expireDate, key)
-	So(err, ShouldBeNil)
+	if expires < 0 {
+		expireDate := time.Now().Add(time.Second * time.Duration(expires))
+		_, err = sqlstore.engine.Exec("UPDATE dashboard_snapshot SET expires = ? WHERE id = ?", expireDate, cmd.Result.Id)
+		So(err, ShouldBeNil)
+	}
 
 
 	return cmd.Result
 	return cmd.Result
 }
 }

+ 2 - 8
pkg/services/sqlstore/migrations/annotation_mig.go

@@ -86,10 +86,7 @@ func addAnnotationMig(mg *Migrator) {
 	// clear alert text
 	// clear alert text
 	//
 	//
 	updateTextFieldSql := "UPDATE annotation SET TEXT = '' WHERE alert_id > 0"
 	updateTextFieldSql := "UPDATE annotation SET TEXT = '' WHERE alert_id > 0"
-	mg.AddMigration("Update alert annotations and set TEXT to empty", new(RawSqlMigration).
-		Sqlite(updateTextFieldSql).
-		Postgres(updateTextFieldSql).
-		Mysql(updateTextFieldSql))
+	mg.AddMigration("Update alert annotations and set TEXT to empty", NewRawSqlMigration(updateTextFieldSql))
 
 
 	//
 	//
 	// Add a 'created' & 'updated' column
 	// Add a 'created' & 'updated' column
@@ -111,8 +108,5 @@ func addAnnotationMig(mg *Migrator) {
 	// Convert epoch saved as seconds to miliseconds
 	// Convert epoch saved as seconds to miliseconds
 	//
 	//
 	updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999"
 	updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999"
-	mg.AddMigration("Convert existing annotations from seconds to milliseconds", new(RawSqlMigration).
-		Sqlite(updateEpochSql).
-		Postgres(updateEpochSql).
-		Mysql(updateEpochSql))
+	mg.AddMigration("Convert existing annotations from seconds to milliseconds", NewRawSqlMigration(updateEpochSql))
 }
 }

+ 1 - 4
pkg/services/sqlstore/migrations/dashboard_acl.go

@@ -45,8 +45,5 @@ INSERT INTO dashboard_acl
 		(-1,-1, 2,'Editor','2017-06-20','2017-06-20')
 		(-1,-1, 2,'Editor','2017-06-20','2017-06-20')
 	`
 	`
 
 
-	mg.AddMigration("save default acl rules in dashboard_acl table", new(RawSqlMigration).
-		Sqlite(rawSQL).
-		Postgres(rawSQL).
-		Mysql(rawSQL))
+	mg.AddMigration("save default acl rules in dashboard_acl table", NewRawSqlMigration(rawSQL))
 }
 }

+ 2 - 4
pkg/services/sqlstore/migrations/dashboard_mig.go

@@ -90,9 +90,7 @@ func addDashboardMigration(mg *Migrator) {
 	mg.AddMigration("drop table dashboard_v1", NewDropTableMigration("dashboard_v1"))
 	mg.AddMigration("drop table dashboard_v1", NewDropTableMigration("dashboard_v1"))
 
 
 	// change column type of dashboard.data
 	// change column type of dashboard.data
-	mg.AddMigration("alter dashboard.data to mediumtext v1", new(RawSqlMigration).
-		Sqlite("SELECT 0 WHERE 0;").
-		Postgres("SELECT 0;").
+	mg.AddMigration("alter dashboard.data to mediumtext v1", NewRawSqlMigration("").
 		Mysql("ALTER TABLE dashboard MODIFY data MEDIUMTEXT;"))
 		Mysql("ALTER TABLE dashboard MODIFY data MEDIUMTEXT;"))
 
 
 	// add column to store updater of a dashboard
 	// add column to store updater of a dashboard
@@ -157,7 +155,7 @@ func addDashboardMigration(mg *Migrator) {
 		Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: true,
 		Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: true,
 	}))
 	}))
 
 
-	mg.AddMigration("Update uid column values in dashboard", new(RawSqlMigration).
+	mg.AddMigration("Update uid column values in dashboard", NewRawSqlMigration("").
 		Sqlite("UPDATE dashboard SET uid=printf('%09d',id) WHERE uid IS NULL;").
 		Sqlite("UPDATE dashboard SET uid=printf('%09d',id) WHERE uid IS NULL;").
 		Postgres("UPDATE dashboard SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;").
 		Postgres("UPDATE dashboard SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;").
 		Mysql("UPDATE dashboard SET uid=lpad(id,9,'0') WHERE uid IS NULL;"))
 		Mysql("UPDATE dashboard SET uid=lpad(id,9,'0') WHERE uid IS NULL;"))

+ 1 - 3
pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go

@@ -50,9 +50,7 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
 	addTableIndicesMigrations(mg, "v5", snapshotV5)
 	addTableIndicesMigrations(mg, "v5", snapshotV5)
 
 
 	// change column type of dashboard
 	// change column type of dashboard
-	mg.AddMigration("alter dashboard_snapshot to mediumtext v2", new(RawSqlMigration).
-		Sqlite("SELECT 0 WHERE 0;").
-		Postgres("SELECT 0;").
+	mg.AddMigration("alter dashboard_snapshot to mediumtext v2", NewRawSqlMigration("").
 		Mysql("ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;"))
 		Mysql("ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;"))
 
 
 	mg.AddMigration("Update dashboard_snapshot table charset", NewTableCharsetMigration("dashboard_snapshot", []*Column{
 	mg.AddMigration("Update dashboard_snapshot table charset", NewTableCharsetMigration("dashboard_snapshot", []*Column{

+ 3 - 11
pkg/services/sqlstore/migrations/dashboard_version_mig.go

@@ -28,10 +28,7 @@ func addDashboardVersionMigration(mg *Migrator) {
 
 
 	// before new dashboards where created with version 0, now they are always inserted with version 1
 	// before new dashboards where created with version 0, now they are always inserted with version 1
 	const setVersionTo1WhereZeroSQL = `UPDATE dashboard SET version = 1 WHERE version = 0`
 	const setVersionTo1WhereZeroSQL = `UPDATE dashboard SET version = 1 WHERE version = 0`
-	mg.AddMigration("Set dashboard version to 1 where 0", new(RawSqlMigration).
-		Sqlite(setVersionTo1WhereZeroSQL).
-		Postgres(setVersionTo1WhereZeroSQL).
-		Mysql(setVersionTo1WhereZeroSQL))
+	mg.AddMigration("Set dashboard version to 1 where 0", NewRawSqlMigration(setVersionTo1WhereZeroSQL))
 
 
 	const rawSQL = `INSERT INTO dashboard_version
 	const rawSQL = `INSERT INTO dashboard_version
 (
 (
@@ -54,14 +51,9 @@ SELECT
 	'',
 	'',
 	dashboard.data
 	dashboard.data
 FROM dashboard;`
 FROM dashboard;`
-	mg.AddMigration("save existing dashboard data in dashboard_version table v1", new(RawSqlMigration).
-		Sqlite(rawSQL).
-		Postgres(rawSQL).
-		Mysql(rawSQL))
+	mg.AddMigration("save existing dashboard data in dashboard_version table v1", NewRawSqlMigration(rawSQL))
 
 
 	// change column type of dashboard_version.data
 	// change column type of dashboard_version.data
-	mg.AddMigration("alter dashboard_version.data to mediumtext v1", new(RawSqlMigration).
-		Sqlite("SELECT 0 WHERE 0;").
-		Postgres("SELECT 0;").
+	mg.AddMigration("alter dashboard_version.data to mediumtext v1", NewRawSqlMigration("").
 		Mysql("ALTER TABLE dashboard_version MODIFY data MEDIUMTEXT;"))
 		Mysql("ALTER TABLE dashboard_version MODIFY data MEDIUMTEXT;"))
 }
 }

+ 1 - 4
pkg/services/sqlstore/migrations/datasource_mig.go

@@ -122,10 +122,7 @@ func addDataSourceMigration(mg *Migrator) {
 	}))
 	}))
 
 
 	const setVersionToOneWhereZero = `UPDATE data_source SET version = 1 WHERE version = 0`
 	const setVersionToOneWhereZero = `UPDATE data_source SET version = 1 WHERE version = 0`
-	mg.AddMigration("Update initial version to 1", new(RawSqlMigration).
-		Sqlite(setVersionToOneWhereZero).
-		Postgres(setVersionToOneWhereZero).
-		Mysql(setVersionToOneWhereZero))
+	mg.AddMigration("Update initial version to 1", NewRawSqlMigration(setVersionToOneWhereZero))
 
 
 	mg.AddMigration("Add read_only data column", NewAddColumnMigration(tableV2, &Column{
 	mg.AddMigration("Add read_only data column", NewAddColumnMigration(tableV2, &Column{
 		Name: "read_only", Type: DB_Bool, Nullable: true,
 		Name: "read_only", Type: DB_Bool, Nullable: true,

+ 2 - 2
pkg/services/sqlstore/migrations/migrations_test.go

@@ -25,7 +25,7 @@ func TestMigrations(t *testing.T) {
 			x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
 			x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			sqlutil.CleanDB(x)
+			NewDialect(x).CleanDB()
 
 
 			_, err = x.SQL(sql).Get(&r)
 			_, err = x.SQL(sql).Get(&r)
 			So(err, ShouldNotBeNil)
 			So(err, ShouldNotBeNil)
@@ -39,7 +39,7 @@ func TestMigrations(t *testing.T) {
 			has, err := x.SQL(sql).Get(&r)
 			has, err := x.SQL(sql).Get(&r)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(has, ShouldBeTrue)
 			So(has, ShouldBeTrue)
-			expectedMigrations := mg.MigrationsCount() - 2 //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this
+			expectedMigrations := mg.MigrationsCount() //we currently skip to migrations. We should rewrite skipped migrations to write in the log as well. until then we have to keep this
 			So(r.Count, ShouldEqual, expectedMigrations)
 			So(r.Count, ShouldEqual, expectedMigrations)
 
 
 			mg = NewMigrator(x)
 			mg = NewMigrator(x)

+ 1 - 25
pkg/services/sqlstore/migrations/org_mig.go

@@ -48,27 +48,6 @@ func addOrgMigrations(mg *Migrator) {
 	mg.AddMigration("create org_user table v1", NewAddTableMigration(orgUserV1))
 	mg.AddMigration("create org_user table v1", NewAddTableMigration(orgUserV1))
 	addTableIndicesMigrations(mg, "v1", orgUserV1)
 	addTableIndicesMigrations(mg, "v1", orgUserV1)
 
 
-	//-------  copy data from old table-------------------
-	mg.AddMigration("copy data account to org", NewCopyTableDataMigration("org", "account", map[string]string{
-		"id":      "id",
-		"version": "version",
-		"name":    "name",
-		"created": "created",
-		"updated": "updated",
-	}).IfTableExists("account"))
-
-	mg.AddMigration("copy data account_user to org_user", NewCopyTableDataMigration("org_user", "account_user", map[string]string{
-		"id":      "id",
-		"org_id":  "account_id",
-		"user_id": "user_id",
-		"role":    "role",
-		"created": "created",
-		"updated": "updated",
-	}).IfTableExists("account_user"))
-
-	mg.AddMigration("Drop old table account", NewDropTableMigration("account"))
-	mg.AddMigration("Drop old table account_user", NewDropTableMigration("account_user"))
-
 	mg.AddMigration("Update org table charset", NewTableCharsetMigration("org", []*Column{
 	mg.AddMigration("Update org table charset", NewTableCharsetMigration("org", []*Column{
 		{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
 		{Name: "name", Type: DB_NVarchar, Length: 190, Nullable: false},
 		{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true},
 		{Name: "address1", Type: DB_NVarchar, Length: 255, Nullable: true},
@@ -85,8 +64,5 @@ func addOrgMigrations(mg *Migrator) {
 	}))
 	}))
 
 
 	const migrateReadOnlyViewersToViewers = `UPDATE org_user SET role = 'Viewer' WHERE role = 'Read Only Editor'`
 	const migrateReadOnlyViewersToViewers = `UPDATE org_user SET role = 'Viewer' WHERE role = 'Read Only Editor'`
-	mg.AddMigration("Migrate all Read Only Viewers to Viewers", new(RawSqlMigration).
-		Sqlite(migrateReadOnlyViewersToViewers).
-		Postgres(migrateReadOnlyViewersToViewers).
-		Mysql(migrateReadOnlyViewersToViewers))
+	mg.AddMigration("Migrate all Read Only Viewers to Viewers", NewRawSqlMigration(migrateReadOnlyViewersToViewers))
 }
 }

+ 3 - 4
pkg/services/sqlstore/migrations/user_auth_mig.go

@@ -22,8 +22,7 @@ func addUserAuthMigrations(mg *Migrator) {
 	// add indices
 	// add indices
 	addTableIndicesMigrations(mg, "v1", userAuthV1)
 	addTableIndicesMigrations(mg, "v1", userAuthV1)
 
 
-	mg.AddMigration("alter user_auth.auth_id to length 255", new(RawSqlMigration).
-		Sqlite("SELECT 0 WHERE 0;").
-		Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(255);").
-		Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(255);"))
+	mg.AddMigration("alter user_auth.auth_id to length 190", NewRawSqlMigration("").
+		Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(190);").
+		Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(190);"))
 }
 }

+ 2 - 41
pkg/services/sqlstore/migrator/column.go

@@ -15,48 +15,9 @@ type Column struct {
 }
 }
 
 
 func (col *Column) String(d Dialect) string {
 func (col *Column) String(d Dialect) string {
-	sql := d.QuoteStr() + col.Name + d.QuoteStr() + " "
-
-	sql += d.SqlType(col) + " "
-
-	if col.IsPrimaryKey {
-		sql += "PRIMARY KEY "
-		if col.IsAutoIncrement {
-			sql += d.AutoIncrStr() + " "
-		}
-	}
-
-	if d.ShowCreateNull() {
-		if col.Nullable {
-			sql += "NULL "
-		} else {
-			sql += "NOT NULL "
-		}
-	}
-
-	if col.Default != "" {
-		sql += "DEFAULT " + col.Default + " "
-	}
-
-	return sql
+	return d.ColString(col)
 }
 }
 
 
 func (col *Column) StringNoPk(d Dialect) string {
 func (col *Column) StringNoPk(d Dialect) string {
-	sql := d.QuoteStr() + col.Name + d.QuoteStr() + " "
-
-	sql += d.SqlType(col) + " "
-
-	if d.ShowCreateNull() {
-		if col.Nullable {
-			sql += "NULL "
-		} else {
-			sql += "NOT NULL "
-		}
-	}
-
-	if col.Default != "" {
-		sql += "DEFAULT " + d.Default(col) + " "
-	}
-
-	return sql
+	return d.ColStringNoPk(col)
 }
 }

+ 103 - 11
pkg/services/sqlstore/migrator/dialect.go

@@ -3,11 +3,12 @@ package migrator
 import (
 import (
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
+
+	"github.com/go-xorm/xorm"
 )
 )
 
 
 type Dialect interface {
 type Dialect interface {
 	DriverName() string
 	DriverName() string
-	QuoteStr() string
 	Quote(string) string
 	Quote(string) string
 	AndStr() string
 	AndStr() string
 	AutoIncrStr() string
 	AutoIncrStr() string
@@ -31,16 +32,29 @@ type Dialect interface {
 	TableCheckSql(tableName string) (string, []interface{})
 	TableCheckSql(tableName string) (string, []interface{})
 	RenameTable(oldName string, newName string) string
 	RenameTable(oldName string, newName string) string
 	UpdateTableSql(tableName string, columns []*Column) string
 	UpdateTableSql(tableName string, columns []*Column) string
+
+	ColString(*Column) string
+	ColStringNoPk(*Column) string
+
+	Limit(limit int64) string
+	LimitOffset(limit int64, offset int64) string
+
+	PreInsertId(table string, sess *xorm.Session) error
+	PostInsertId(table string, sess *xorm.Session) error
+
+	CleanDB() error
+	NoOpSql() string
 }
 }
 
 
-func NewDialect(name string) Dialect {
+func NewDialect(engine *xorm.Engine) Dialect {
+	name := engine.DriverName()
 	switch name {
 	switch name {
 	case MYSQL:
 	case MYSQL:
-		return NewMysqlDialect()
+		return NewMysqlDialect(engine)
 	case SQLITE:
 	case SQLITE:
-		return NewSqlite3Dialect()
+		return NewSqlite3Dialect(engine)
 	case POSTGRES:
 	case POSTGRES:
-		return NewPostgresDialect()
+		return NewPostgresDialect(engine)
 	}
 	}
 
 
 	panic("Unsupported database type: " + name)
 	panic("Unsupported database type: " + name)
@@ -48,6 +62,7 @@ func NewDialect(name string) Dialect {
 
 
 type BaseDialect struct {
 type BaseDialect struct {
 	dialect    Dialect
 	dialect    Dialect
+	engine     *xorm.Engine
 	driverName string
 	driverName string
 }
 }
 
 
@@ -100,9 +115,12 @@ func (b *BaseDialect) CreateTableSql(table *Table) string {
 	}
 	}
 
 
 	if len(pkList) > 1 {
 	if len(pkList) > 1 {
-		sql += "PRIMARY KEY ( "
-		sql += b.dialect.Quote(strings.Join(pkList, b.dialect.Quote(",")))
-		sql += " ), "
+		quotedCols := []string{}
+		for _, col := range pkList {
+			quotedCols = append(quotedCols, b.dialect.Quote(col))
+		}
+
+		sql += "PRIMARY KEY ( " + strings.Join(quotedCols, ",") + " ), "
 	}
 	}
 
 
 	sql = sql[:len(sql)-2] + ")"
 	sql = sql[:len(sql)-2] + ")"
@@ -127,9 +145,12 @@ func (db *BaseDialect) CreateIndexSql(tableName string, index *Index) string {
 
 
 	idxName := index.XName(tableName)
 	idxName := index.XName(tableName)
 
 
-	return fmt.Sprintf("CREATE%s INDEX %v ON %v (%v);", unique,
-		quote(idxName), quote(tableName),
-		quote(strings.Join(index.Cols, quote(","))))
+	quotedCols := []string{}
+	for _, col := range index.Cols {
+		quotedCols = append(quotedCols, db.dialect.Quote(col))
+	}
+
+	return fmt.Sprintf("CREATE%s INDEX %v ON %v (%v);", unique, quote(idxName), quote(tableName), strings.Join(quotedCols, ","))
 }
 }
 
 
 func (db *BaseDialect) QuoteColList(cols []string) string {
 func (db *BaseDialect) QuoteColList(cols []string) string {
@@ -168,3 +189,74 @@ func (db *BaseDialect) DropIndexSql(tableName string, index *Index) string {
 func (db *BaseDialect) UpdateTableSql(tableName string, columns []*Column) string {
 func (db *BaseDialect) UpdateTableSql(tableName string, columns []*Column) string {
 	return "-- NOT REQUIRED"
 	return "-- NOT REQUIRED"
 }
 }
+
+func (db *BaseDialect) ColString(col *Column) string {
+	sql := db.dialect.Quote(col.Name) + " "
+
+	sql += db.dialect.SqlType(col) + " "
+
+	if col.IsPrimaryKey {
+		sql += "PRIMARY KEY "
+		if col.IsAutoIncrement {
+			sql += db.dialect.AutoIncrStr() + " "
+		}
+	}
+
+	if db.dialect.ShowCreateNull() {
+		if col.Nullable {
+			sql += "NULL "
+		} else {
+			sql += "NOT NULL "
+		}
+	}
+
+	if col.Default != "" {
+		sql += "DEFAULT " + db.dialect.Default(col) + " "
+	}
+
+	return sql
+}
+
+func (db *BaseDialect) ColStringNoPk(col *Column) string {
+	sql := db.dialect.Quote(col.Name) + " "
+
+	sql += db.dialect.SqlType(col) + " "
+
+	if db.dialect.ShowCreateNull() {
+		if col.Nullable {
+			sql += "NULL "
+		} else {
+			sql += "NOT NULL "
+		}
+	}
+
+	if col.Default != "" {
+		sql += "DEFAULT " + db.dialect.Default(col) + " "
+	}
+
+	return sql
+}
+
+func (db *BaseDialect) Limit(limit int64) string {
+	return fmt.Sprintf(" LIMIT %d", limit)
+}
+
+func (db *BaseDialect) LimitOffset(limit int64, offset int64) string {
+	return fmt.Sprintf(" LIMIT %d OFFSET %d", limit, offset)
+}
+
+func (db *BaseDialect) PreInsertId(table string, sess *xorm.Session) error {
+	return nil
+}
+
+func (db *BaseDialect) PostInsertId(table string, sess *xorm.Session) error {
+	return nil
+}
+
+func (db *BaseDialect) CleanDB() error {
+	return nil
+}
+
+func (db *BaseDialect) NoOpSql() string {
+	return "SELECT 0;"
+}

+ 38 - 17
pkg/services/sqlstore/migrator/migrations.go

@@ -24,37 +24,58 @@ func (m *MigrationBase) GetCondition() MigrationCondition {
 type RawSqlMigration struct {
 type RawSqlMigration struct {
 	MigrationBase
 	MigrationBase
 
 
-	sqlite   string
-	mysql    string
-	postgres string
+	sql map[string]string
+}
+
+func NewRawSqlMigration(sql string) *RawSqlMigration {
+	m := &RawSqlMigration{}
+	if sql != "" {
+		m.Default(sql)
+	}
+	return m
 }
 }
 
 
 func (m *RawSqlMigration) Sql(dialect Dialect) string {
 func (m *RawSqlMigration) Sql(dialect Dialect) string {
-	switch dialect.DriverName() {
-	case MYSQL:
-		return m.mysql
-	case SQLITE:
-		return m.sqlite
-	case POSTGRES:
-		return m.postgres
+	if m.sql != nil {
+		if val := m.sql[dialect.DriverName()]; val != "" {
+			return val
+		}
+
+		if val := m.sql["default"]; val != "" {
+			return val
+		}
 	}
 	}
 
 
-	panic("db type not supported")
+	return dialect.NoOpSql()
 }
 }
 
 
-func (m *RawSqlMigration) Sqlite(sql string) *RawSqlMigration {
-	m.sqlite = sql
+func (m *RawSqlMigration) Set(dialect string, sql string) *RawSqlMigration {
+	if m.sql == nil {
+		m.sql = make(map[string]string)
+	}
+
+	m.sql[dialect] = sql
 	return m
 	return m
 }
 }
 
 
+func (m *RawSqlMigration) Default(sql string) *RawSqlMigration {
+	return m.Set("default", sql)
+}
+
+func (m *RawSqlMigration) Sqlite(sql string) *RawSqlMigration {
+	return m.Set(SQLITE, sql)
+}
+
 func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration {
 func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration {
-	m.mysql = sql
-	return m
+	return m.Set(MYSQL, sql)
 }
 }
 
 
 func (m *RawSqlMigration) Postgres(sql string) *RawSqlMigration {
 func (m *RawSqlMigration) Postgres(sql string) *RawSqlMigration {
-	m.postgres = sql
-	return m
+	return m.Set(POSTGRES, sql)
+}
+
+func (m *RawSqlMigration) Mssql(sql string) *RawSqlMigration {
+	return m.Set(MSSQL, sql)
 }
 }
 
 
 type AddColumnMigration struct {
 type AddColumnMigration struct {

+ 2 - 2
pkg/services/sqlstore/migrator/migrator.go

@@ -31,7 +31,7 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
 	mg.x = engine
 	mg.x = engine
 	mg.Logger = log.New("migrator")
 	mg.Logger = log.New("migrator")
 	mg.migrations = make([]Migration, 0)
 	mg.migrations = make([]Migration, 0)
-	mg.dialect = NewDialect(mg.x.DriverName())
+	mg.dialect = NewDialect(mg.x)
 	return mg
 	return mg
 }
 }
 
 
@@ -125,7 +125,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
 		sql, args := condition.Sql(mg.dialect)
 		sql, args := condition.Sql(mg.dialect)
 		results, err := sess.SQL(sql).Query(args...)
 		results, err := sess.SQL(sql).Query(args...)
 		if err != nil || len(results) == 0 {
 		if err != nil || len(results) == 0 {
-			mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id())
+			mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id())
 			return sess.Rollback()
 			return sess.Rollback()
 		}
 		}
 	}
 	}

+ 25 - 5
pkg/services/sqlstore/migrator/mysql_dialect.go

@@ -1,17 +1,21 @@
 package migrator
 package migrator
 
 
 import (
 import (
+	"fmt"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
+
+	"github.com/go-xorm/xorm"
 )
 )
 
 
 type Mysql struct {
 type Mysql struct {
 	BaseDialect
 	BaseDialect
 }
 }
 
 
-func NewMysqlDialect() *Mysql {
+func NewMysqlDialect(engine *xorm.Engine) *Mysql {
 	d := Mysql{}
 	d := Mysql{}
 	d.BaseDialect.dialect = &d
 	d.BaseDialect.dialect = &d
+	d.BaseDialect.engine = engine
 	d.BaseDialect.driverName = MYSQL
 	d.BaseDialect.driverName = MYSQL
 	return &d
 	return &d
 }
 }
@@ -24,10 +28,6 @@ func (db *Mysql) Quote(name string) string {
 	return "`" + name + "`"
 	return "`" + name + "`"
 }
 }
 
 
-func (db *Mysql) QuoteStr() string {
-	return "`"
-}
-
 func (db *Mysql) AutoIncrStr() string {
 func (db *Mysql) AutoIncrStr() string {
 	return "AUTO_INCREMENT"
 	return "AUTO_INCREMENT"
 }
 }
@@ -105,3 +105,23 @@ func (db *Mysql) UpdateTableSql(tableName string, columns []*Column) string {
 
 
 	return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
 	return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
 }
 }
+
+func (db *Mysql) CleanDB() error {
+	tables, _ := db.engine.DBMetas()
+	sess := db.engine.NewSession()
+	defer sess.Close()
+
+	for _, table := range tables {
+		if _, err := sess.Exec("set foreign_key_checks = 0"); err != nil {
+			return fmt.Errorf("failed to disable foreign key checks")
+		}
+		if _, err := sess.Exec("drop table " + table.Name + " ;"); err != nil {
+			return fmt.Errorf("failed to delete table: %v, err: %v", table.Name, err)
+		}
+		if _, err := sess.Exec("set foreign_key_checks = 1"); err != nil {
+			return fmt.Errorf("failed to disable foreign key checks")
+		}
+	}
+
+	return nil
+}

+ 20 - 6
pkg/services/sqlstore/migrator/postgres_dialect.go

@@ -4,15 +4,18 @@ import (
 	"fmt"
 	"fmt"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
+
+	"github.com/go-xorm/xorm"
 )
 )
 
 
 type Postgres struct {
 type Postgres struct {
 	BaseDialect
 	BaseDialect
 }
 }
 
 
-func NewPostgresDialect() *Postgres {
+func NewPostgresDialect(engine *xorm.Engine) *Postgres {
 	d := Postgres{}
 	d := Postgres{}
 	d.BaseDialect.dialect = &d
 	d.BaseDialect.dialect = &d
+	d.BaseDialect.engine = engine
 	d.BaseDialect.driverName = POSTGRES
 	d.BaseDialect.driverName = POSTGRES
 	return &d
 	return &d
 }
 }
@@ -25,10 +28,6 @@ func (db *Postgres) Quote(name string) string {
 	return "\"" + name + "\""
 	return "\"" + name + "\""
 }
 }
 
 
-func (db *Postgres) QuoteStr() string {
-	return "\""
-}
-
 func (b *Postgres) LikeStr() string {
 func (b *Postgres) LikeStr() string {
 	return "ILIKE"
 	return "ILIKE"
 }
 }
@@ -117,8 +116,23 @@ func (db *Postgres) UpdateTableSql(tableName string, columns []*Column) string {
 	var statements = []string{}
 	var statements = []string{}
 
 
 	for _, col := range columns {
 	for _, col := range columns {
-		statements = append(statements, "ALTER "+db.QuoteStr()+col.Name+db.QuoteStr()+" TYPE "+db.SqlType(col))
+		statements = append(statements, "ALTER "+db.Quote(col.Name)+" TYPE "+db.SqlType(col))
 	}
 	}
 
 
 	return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
 	return "ALTER TABLE " + db.Quote(tableName) + " " + strings.Join(statements, ", ") + ";"
 }
 }
+
+func (db *Postgres) CleanDB() error {
+	sess := db.engine.NewSession()
+	defer sess.Close()
+
+	if _, err := sess.Exec("DROP SCHEMA public CASCADE;"); err != nil {
+		return fmt.Errorf("Failed to drop schema public")
+	}
+
+	if _, err := sess.Exec("CREATE SCHEMA public;"); err != nil {
+		return fmt.Errorf("Failed to create schema public")
+	}
+
+	return nil
+}

+ 11 - 6
pkg/services/sqlstore/migrator/sqlite_dialect.go

@@ -1,14 +1,19 @@
 package migrator
 package migrator
 
 
-import "fmt"
+import (
+	"fmt"
+
+	"github.com/go-xorm/xorm"
+)
 
 
 type Sqlite3 struct {
 type Sqlite3 struct {
 	BaseDialect
 	BaseDialect
 }
 }
 
 
-func NewSqlite3Dialect() *Sqlite3 {
+func NewSqlite3Dialect(engine *xorm.Engine) *Sqlite3 {
 	d := Sqlite3{}
 	d := Sqlite3{}
 	d.BaseDialect.dialect = &d
 	d.BaseDialect.dialect = &d
+	d.BaseDialect.engine = engine
 	d.BaseDialect.driverName = SQLITE
 	d.BaseDialect.driverName = SQLITE
 	return &d
 	return &d
 }
 }
@@ -21,10 +26,6 @@ func (db *Sqlite3) Quote(name string) string {
 	return "`" + name + "`"
 	return "`" + name + "`"
 }
 }
 
 
-func (db *Sqlite3) QuoteStr() string {
-	return "`"
-}
-
 func (db *Sqlite3) AutoIncrStr() string {
 func (db *Sqlite3) AutoIncrStr() string {
 	return "AUTOINCREMENT"
 	return "AUTOINCREMENT"
 }
 }
@@ -77,3 +78,7 @@ func (db *Sqlite3) DropIndexSql(tableName string, index *Index) string {
 	idxName := index.XName(tableName)
 	idxName := index.XName(tableName)
 	return fmt.Sprintf("DROP INDEX %v", quote(idxName))
 	return fmt.Sprintf("DROP INDEX %v", quote(idxName))
 }
 }
+
+func (db *Sqlite3) CleanDB() error {
+	return nil
+}

+ 1 - 0
pkg/services/sqlstore/migrator/types.go

@@ -9,6 +9,7 @@ const (
 	POSTGRES = "postgres"
 	POSTGRES = "postgres"
 	SQLITE   = "sqlite3"
 	SQLITE   = "sqlite3"
 	MYSQL    = "mysql"
 	MYSQL    = "mysql"
+	MSSQL    = "mssql"
 )
 )
 
 
 type Migration interface {
 type Migration interface {

+ 1 - 1
pkg/services/sqlstore/playlist.go

@@ -64,7 +64,7 @@ func UpdatePlaylist(cmd *m.UpdatePlaylistCommand) error {
 		Interval: playlist.Interval,
 		Interval: playlist.Interval,
 	}
 	}
 
 
-	_, err := x.ID(cmd.Id).Cols("id", "name", "interval").Update(&playlist)
+	_, err := x.ID(cmd.Id).Cols("name", "interval").Update(&playlist)
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 1 - 0
pkg/services/sqlstore/quota_test.go

@@ -43,6 +43,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
 			Name:   "TestOrg",
 			Name:   "TestOrg",
 			UserId: 1,
 			UserId: 1,
 		}
 		}
+
 		err := CreateOrg(&userCmd)
 		err := CreateOrg(&userCmd)
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 		orgId = userCmd.Result.Id
 		orgId = userCmd.Result.Id

+ 3 - 5
pkg/services/sqlstore/search_builder.go

@@ -92,7 +92,7 @@ func (sb *SearchBuilder) ToSql() (string, []interface{}) {
 		LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id
 		LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id
 		LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`)
 		LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`)
 
 
-	sb.sql.WriteString(" ORDER BY dashboard.title ASC LIMIT 5000")
+	sb.sql.WriteString(" ORDER BY dashboard.title ASC" + dialect.Limit(5000))
 
 
 	return sb.sql.String(), sb.params
 	return sb.sql.String(), sb.params
 }
 }
@@ -135,12 +135,11 @@ func (sb *SearchBuilder) buildTagQuery() {
 	// this ends the inner select (tag filtered part)
 	// this ends the inner select (tag filtered part)
 	sb.sql.WriteString(`
 	sb.sql.WriteString(`
 		GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
 		GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
-		LIMIT ?) as ids
+		ORDER BY dashboard.id` + dialect.Limit(int64(sb.limit)) + `) as ids
 		INNER JOIN dashboard on ids.id = dashboard.id
 		INNER JOIN dashboard on ids.id = dashboard.id
 	`)
 	`)
 
 
 	sb.params = append(sb.params, len(sb.tags))
 	sb.params = append(sb.params, len(sb.tags))
-	sb.params = append(sb.params, sb.limit)
 }
 }
 
 
 func (sb *SearchBuilder) buildMainQuery() {
 func (sb *SearchBuilder) buildMainQuery() {
@@ -153,8 +152,7 @@ func (sb *SearchBuilder) buildMainQuery() {
 	sb.sql.WriteString(` WHERE `)
 	sb.sql.WriteString(` WHERE `)
 	sb.buildSearchWhereClause()
 	sb.buildSearchWhereClause()
 
 
-	sb.sql.WriteString(` LIMIT ?) as ids INNER JOIN dashboard on ids.id = dashboard.id `)
-	sb.params = append(sb.params, sb.limit)
+	sb.sql.WriteString(` ORDER BY dashboard.title` + dialect.Limit(int64(sb.limit)) + `) as ids INNER JOIN dashboard on ids.id = dashboard.id `)
 }
 }
 
 
 func (sb *SearchBuilder) buildSearchWhereClause() {
 func (sb *SearchBuilder) buildSearchWhereClause() {

+ 2 - 5
pkg/services/sqlstore/search_builder_test.go

@@ -4,13 +4,10 @@ import (
 	"testing"
 	"testing"
 
 
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
 func TestSearchBuilder(t *testing.T) {
 func TestSearchBuilder(t *testing.T) {
-	dialect = migrator.NewDialect("sqlite3")
-
 	Convey("Testing building a search", t, func() {
 	Convey("Testing building a search", t, func() {
 		signedInUser := &m.SignedInUser{
 		signedInUser := &m.SignedInUser{
 			OrgId:  1,
 			OrgId:  1,
@@ -23,7 +20,7 @@ func TestSearchBuilder(t *testing.T) {
 			sql, params := sb.IsStarred().WithTitle("test").ToSql()
 			sql, params := sb.IsStarred().WithTitle("test").ToSql()
 			So(sql, ShouldStartWith, "SELECT")
 			So(sql, ShouldStartWith, "SELECT")
 			So(sql, ShouldContainSubstring, "INNER JOIN dashboard on ids.id = dashboard.id")
 			So(sql, ShouldContainSubstring, "INNER JOIN dashboard on ids.id = dashboard.id")
-			So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000")
+			So(sql, ShouldContainSubstring, "ORDER BY dashboard.title ASC")
 			So(len(params), ShouldBeGreaterThan, 0)
 			So(len(params), ShouldBeGreaterThan, 0)
 		})
 		})
 
 
@@ -31,7 +28,7 @@ func TestSearchBuilder(t *testing.T) {
 			sql, params := sb.WithTags([]string{"tag1", "tag2"}).ToSql()
 			sql, params := sb.WithTags([]string{"tag1", "tag2"}).ToSql()
 			So(sql, ShouldStartWith, "SELECT")
 			So(sql, ShouldStartWith, "SELECT")
 			So(sql, ShouldContainSubstring, "LEFT OUTER JOIN dashboard_tag")
 			So(sql, ShouldContainSubstring, "LEFT OUTER JOIN dashboard_tag")
-			So(sql, ShouldEndWith, "ORDER BY dashboard.title ASC LIMIT 5000")
+			So(sql, ShouldContainSubstring, "ORDER BY dashboard.title ASC")
 			So(len(params), ShouldBeGreaterThan, 0)
 			So(len(params), ShouldBeGreaterThan, 0)
 		})
 		})
 	})
 	})

+ 21 - 0
pkg/services/sqlstore/shared.go

@@ -1,6 +1,7 @@
 package sqlstore
 package sqlstore
 
 
 import (
 import (
+	"reflect"
 	"time"
 	"time"
 
 
 	"github.com/go-xorm/xorm"
 	"github.com/go-xorm/xorm"
@@ -67,3 +68,23 @@ func inTransactionWithRetry(callback dbTransactionFunc, retry int) error {
 
 
 	return nil
 	return nil
 }
 }
+
+func (sess *DBSession) InsertId(bean interface{}) (int64, error) {
+	table := sess.DB().Mapper.Obj2Table(getTypeName(bean))
+
+	dialect.PreInsertId(table, sess.Session)
+
+	id, err := sess.Session.InsertOne(bean)
+
+	dialect.PostInsertId(table, sess.Session)
+
+	return id, err
+}
+
+func getTypeName(bean interface{}) (res string) {
+	t := reflect.TypeOf(bean)
+	for t.Kind() == reflect.Ptr {
+		t = t.Elem()
+	}
+	return t.Name()
+}

+ 167 - 144
pkg/services/sqlstore/sqlstore.go

@@ -13,6 +13,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/services/annotations"
 	"github.com/grafana/grafana/pkg/services/annotations"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@@ -20,7 +21,6 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/go-sql-driver/mysql"
-	_ "github.com/go-sql-driver/mysql"
 	"github.com/go-xorm/xorm"
 	"github.com/go-xorm/xorm"
 	_ "github.com/lib/pq"
 	_ "github.com/lib/pq"
 	_ "github.com/mattn/go-sqlite3"
 	_ "github.com/mattn/go-sqlite3"
@@ -28,39 +28,72 @@ import (
 	_ "github.com/grafana/grafana/pkg/tsdb/mssql"
 	_ "github.com/grafana/grafana/pkg/tsdb/mssql"
 )
 )
 
 
-type DatabaseConfig struct {
-	Type, Host, Name, User, Pwd, Path, SslMode string
-	CaCertPath                                 string
-	ClientKeyPath                              string
-	ClientCertPath                             string
-	ServerCertName                             string
-	MaxOpenConn                                int
-	MaxIdleConn                                int
-	ConnMaxLifetime                            int
-}
-
 var (
 var (
 	x       *xorm.Engine
 	x       *xorm.Engine
 	dialect migrator.Dialect
 	dialect migrator.Dialect
 
 
-	HasEngine bool
+	sqlog log.Logger = log.New("sqlstore")
+)
+
+func init() {
+	registry.Register(&registry.Descriptor{
+		Name:         "SqlStore",
+		Instance:     &SqlStore{},
+		InitPriority: registry.High,
+	})
+}
 
 
-	DbCfg DatabaseConfig
+type SqlStore struct {
+	Cfg *setting.Cfg `inject:""`
 
 
-	UseSQLite3 bool
-	sqlog      log.Logger = log.New("sqlstore")
-)
+	dbCfg           DatabaseConfig
+	engine          *xorm.Engine
+	log             log.Logger
+	skipEnsureAdmin bool
+}
+
+func (ss *SqlStore) Init() error {
+	ss.log = log.New("sqlstore")
+	ss.readConfig()
+
+	engine, err := ss.getEngine()
 
 
-func EnsureAdminUser() {
+	if err != nil {
+		return fmt.Errorf("Fail to connect to database: %v", err)
+	}
+
+	ss.engine = engine
+
+	// temporarily still set global var
+	x = engine
+	dialect = migrator.NewDialect(x)
+	migrator := migrator.NewMigrator(x)
+	migrations.AddMigrations(migrator)
+
+	if err := migrator.Start(); err != nil {
+		return fmt.Errorf("Migration failed err: %v", err)
+	}
+
+	// Init repo instances
+	annotations.SetRepository(&SqlAnnotationRepo{})
+
+	// ensure admin user
+	if ss.skipEnsureAdmin {
+		return nil
+	}
+
+	return ss.ensureAdminUser()
+}
+
+func (ss *SqlStore) ensureAdminUser() error {
 	statsQuery := m.GetSystemStatsQuery{}
 	statsQuery := m.GetSystemStatsQuery{}
 
 
 	if err := bus.Dispatch(&statsQuery); err != nil {
 	if err := bus.Dispatch(&statsQuery); err != nil {
-		log.Fatal(3, "Could not determine if admin user exists: %v", err)
-		return
+		fmt.Errorf("Could not determine if admin user exists: %v", err)
 	}
 	}
 
 
 	if statsQuery.Result.Users > 0 {
 	if statsQuery.Result.Users > 0 {
-		return
+		return nil
 	}
 	}
 
 
 	cmd := m.CreateUserCommand{}
 	cmd := m.CreateUserCommand{}
@@ -70,105 +103,89 @@ func EnsureAdminUser() {
 	cmd.IsAdmin = true
 	cmd.IsAdmin = true
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
-		log.Error(3, "Failed to create default admin user", err)
-		return
+		return fmt.Errorf("Failed to create admin user: %v", err)
 	}
 	}
 
 
-	log.Info("Created default admin user: %v", setting.AdminUser)
-}
-
-func NewEngine() *xorm.Engine {
-	x, err := getEngine()
-
-	if err != nil {
-		sqlog.Crit("Fail to connect to database", "error", err)
-		os.Exit(1)
-	}
-
-	err = SetEngine(x)
+	ss.log.Info("Created default admin user: %v", setting.AdminUser)
 
 
-	if err != nil {
-		sqlog.Error("Fail to initialize orm engine", "error", err)
-		os.Exit(1)
-	}
-
-	return x
+	return nil
 }
 }
 
 
-func SetEngine(engine *xorm.Engine) (err error) {
-	x = engine
-	dialect = migrator.NewDialect(x.DriverName())
+func (ss *SqlStore) buildConnectionString() (string, error) {
+	cnnstr := ss.dbCfg.ConnectionString
 
 
-	migrator := migrator.NewMigrator(x)
-	migrations.AddMigrations(migrator)
-
-	if err := migrator.Start(); err != nil {
-		return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
+	// special case used by integration tests
+	if cnnstr != "" {
+		return cnnstr, nil
 	}
 	}
 
 
-	// Init repo instances
-	annotations.SetRepository(&SqlAnnotationRepo{})
-	return nil
-}
-
-func getEngine() (*xorm.Engine, error) {
-	LoadConfig()
-
-	cnnstr := ""
-	switch DbCfg.Type {
-	case "mysql":
+	switch ss.dbCfg.Type {
+	case migrator.MYSQL:
 		protocol := "tcp"
 		protocol := "tcp"
-		if strings.HasPrefix(DbCfg.Host, "/") {
+		if strings.HasPrefix(ss.dbCfg.Host, "/") {
 			protocol = "unix"
 			protocol = "unix"
 		}
 		}
 
 
 		cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
 		cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
-			DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
+			ss.dbCfg.User, ss.dbCfg.Pwd, protocol, ss.dbCfg.Host, ss.dbCfg.Name)
 
 
-		if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
-			tlsCert, err := makeCert("custom", DbCfg)
+		if ss.dbCfg.SslMode == "true" || ss.dbCfg.SslMode == "skip-verify" {
+			tlsCert, err := makeCert("custom", ss.dbCfg)
 			if err != nil {
 			if err != nil {
-				return nil, err
+				return "", err
 			}
 			}
 			mysql.RegisterTLSConfig("custom", tlsCert)
 			mysql.RegisterTLSConfig("custom", tlsCert)
 			cnnstr += "&tls=custom"
 			cnnstr += "&tls=custom"
 		}
 		}
-	case "postgres":
+	case migrator.POSTGRES:
 		var host, port = "127.0.0.1", "5432"
 		var host, port = "127.0.0.1", "5432"
-		fields := strings.Split(DbCfg.Host, ":")
+		fields := strings.Split(ss.dbCfg.Host, ":")
 		if len(fields) > 0 && len(strings.TrimSpace(fields[0])) > 0 {
 		if len(fields) > 0 && len(strings.TrimSpace(fields[0])) > 0 {
 			host = fields[0]
 			host = fields[0]
 		}
 		}
 		if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
 		if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 {
 			port = fields[1]
 			port = fields[1]
 		}
 		}
-		if DbCfg.Pwd == "" {
-			DbCfg.Pwd = "''"
+		if ss.dbCfg.Pwd == "" {
+			ss.dbCfg.Pwd = "''"
 		}
 		}
-		if DbCfg.User == "" {
-			DbCfg.User = "''"
+		if ss.dbCfg.User == "" {
+			ss.dbCfg.User = "''"
 		}
 		}
-		cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode, DbCfg.ClientCertPath, DbCfg.ClientKeyPath, DbCfg.CaCertPath)
-	case "sqlite3":
-		if !filepath.IsAbs(DbCfg.Path) {
-			DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path)
+		cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", ss.dbCfg.User, ss.dbCfg.Pwd, host, port, ss.dbCfg.Name, ss.dbCfg.SslMode, ss.dbCfg.ClientCertPath, ss.dbCfg.ClientKeyPath, ss.dbCfg.CaCertPath)
+	case migrator.SQLITE:
+		// special case for tests
+		if !filepath.IsAbs(ss.dbCfg.Path) {
+			ss.dbCfg.Path = filepath.Join(setting.DataPath, ss.dbCfg.Path)
 		}
 		}
-		os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm)
-		cnnstr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc"
+		os.MkdirAll(path.Dir(ss.dbCfg.Path), os.ModePerm)
+		cnnstr = "file:" + ss.dbCfg.Path + "?cache=shared&mode=rwc"
 	default:
 	default:
-		return nil, fmt.Errorf("Unknown database type: %s", DbCfg.Type)
+		return "", fmt.Errorf("Unknown database type: %s", ss.dbCfg.Type)
 	}
 	}
 
 
-	sqlog.Info("Initializing DB", "dbtype", DbCfg.Type)
-	engine, err := xorm.NewEngine(DbCfg.Type, cnnstr)
+	return cnnstr, nil
+}
+
+func (ss *SqlStore) getEngine() (*xorm.Engine, error) {
+	connectionString, err := ss.buildConnectionString()
+
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	engine.SetMaxOpenConns(DbCfg.MaxOpenConn)
-	engine.SetMaxIdleConns(DbCfg.MaxIdleConn)
-	engine.SetConnMaxLifetime(time.Second * time.Duration(DbCfg.ConnMaxLifetime))
-	debugSql := setting.Raw.Section("database").Key("log_queries").MustBool(false)
+	sqlog.Info("Connecting to DB", "dbtype", ss.dbCfg.Type)
+	engine, err := xorm.NewEngine(ss.dbCfg.Type, connectionString)
+	if err != nil {
+		return nil, err
+	}
+
+	engine.SetMaxOpenConns(ss.dbCfg.MaxOpenConn)
+	engine.SetMaxIdleConns(ss.dbCfg.MaxIdleConn)
+	engine.SetConnMaxLifetime(time.Second * time.Duration(ss.dbCfg.ConnMaxLifetime))
+
+	// configure sql logging
+	debugSql := ss.Cfg.Raw.Section("database").Key("log_queries").MustBool(false)
 	if !debugSql {
 	if !debugSql {
 		engine.SetLogger(&xorm.DiscardLogger{})
 		engine.SetLogger(&xorm.DiscardLogger{})
 	} else {
 	} else {
@@ -180,101 +197,95 @@ func getEngine() (*xorm.Engine, error) {
 	return engine, nil
 	return engine, nil
 }
 }
 
 
-func LoadConfig() {
-	sec := setting.Raw.Section("database")
+func (ss *SqlStore) readConfig() {
+	sec := ss.Cfg.Raw.Section("database")
 
 
 	cfgURL := sec.Key("url").String()
 	cfgURL := sec.Key("url").String()
 	if len(cfgURL) != 0 {
 	if len(cfgURL) != 0 {
 		dbURL, _ := url.Parse(cfgURL)
 		dbURL, _ := url.Parse(cfgURL)
-		DbCfg.Type = dbURL.Scheme
-		DbCfg.Host = dbURL.Host
+		ss.dbCfg.Type = dbURL.Scheme
+		ss.dbCfg.Host = dbURL.Host
 
 
 		pathSplit := strings.Split(dbURL.Path, "/")
 		pathSplit := strings.Split(dbURL.Path, "/")
 		if len(pathSplit) > 1 {
 		if len(pathSplit) > 1 {
-			DbCfg.Name = pathSplit[1]
+			ss.dbCfg.Name = pathSplit[1]
 		}
 		}
 
 
 		userInfo := dbURL.User
 		userInfo := dbURL.User
 		if userInfo != nil {
 		if userInfo != nil {
-			DbCfg.User = userInfo.Username()
-			DbCfg.Pwd, _ = userInfo.Password()
+			ss.dbCfg.User = userInfo.Username()
+			ss.dbCfg.Pwd, _ = userInfo.Password()
 		}
 		}
 	} else {
 	} else {
-		DbCfg.Type = sec.Key("type").String()
-		DbCfg.Host = sec.Key("host").String()
-		DbCfg.Name = sec.Key("name").String()
-		DbCfg.User = sec.Key("user").String()
-		if len(DbCfg.Pwd) == 0 {
-			DbCfg.Pwd = sec.Key("password").String()
-		}
+		ss.dbCfg.Type = sec.Key("type").String()
+		ss.dbCfg.Host = sec.Key("host").String()
+		ss.dbCfg.Name = sec.Key("name").String()
+		ss.dbCfg.User = sec.Key("user").String()
+		ss.dbCfg.ConnectionString = sec.Key("connection_string").String()
+		ss.dbCfg.Pwd = sec.Key("password").String()
 	}
 	}
-	DbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
-	DbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(0)
-	DbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400)
-
-	if DbCfg.Type == "sqlite3" {
-		UseSQLite3 = true
-		// only allow one connection as sqlite3 has multi threading issues that cause table locks
-		// DbCfg.MaxIdleConn = 1
-		// DbCfg.MaxOpenConn = 1
-	}
-	DbCfg.SslMode = sec.Key("ssl_mode").String()
-	DbCfg.CaCertPath = sec.Key("ca_cert_path").String()
-	DbCfg.ClientKeyPath = sec.Key("client_key_path").String()
-	DbCfg.ClientCertPath = sec.Key("client_cert_path").String()
-	DbCfg.ServerCertName = sec.Key("server_cert_name").String()
-	DbCfg.Path = sec.Key("path").MustString("data/grafana.db")
-}
 
 
-var (
-	dbSqlite   = "sqlite"
-	dbMySql    = "mysql"
-	dbPostgres = "postgres"
-)
+	ss.dbCfg.MaxOpenConn = sec.Key("max_open_conn").MustInt(0)
+	ss.dbCfg.MaxIdleConn = sec.Key("max_idle_conn").MustInt(2)
+	ss.dbCfg.ConnMaxLifetime = sec.Key("conn_max_lifetime").MustInt(14400)
+
+	ss.dbCfg.SslMode = sec.Key("ssl_mode").String()
+	ss.dbCfg.CaCertPath = sec.Key("ca_cert_path").String()
+	ss.dbCfg.ClientKeyPath = sec.Key("client_key_path").String()
+	ss.dbCfg.ClientCertPath = sec.Key("client_cert_path").String()
+	ss.dbCfg.ServerCertName = sec.Key("server_cert_name").String()
+	ss.dbCfg.Path = sec.Key("path").MustString("data/grafana.db")
+}
 
 
-func InitTestDB(t *testing.T) *xorm.Engine {
-	selectedDb := dbSqlite
-	// selectedDb := dbMySql
-	// selectedDb := dbPostgres
+func InitTestDB(t *testing.T) *SqlStore {
+	sqlstore := &SqlStore{}
+	sqlstore.skipEnsureAdmin = true
 
 
-	var x *xorm.Engine
-	var err error
+	dbType := migrator.SQLITE
 
 
 	// environment variable present for test db?
 	// environment variable present for test db?
 	if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
 	if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
-		selectedDb = db
+		dbType = db
 	}
 	}
 
 
-	switch strings.ToLower(selectedDb) {
-	case dbMySql:
-		x, err = xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
-	case dbPostgres:
-		x, err = xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
+	// set test db config
+	sqlstore.Cfg = setting.NewCfg()
+	sec, _ := sqlstore.Cfg.Raw.NewSection("database")
+	sec.NewKey("type", dbType)
+
+	switch dbType {
+	case "mysql":
+		sec.NewKey("connection_string", sqlutil.TestDB_Mysql.ConnStr)
+	case "postgres":
+		sec.NewKey("connection_string", sqlutil.TestDB_Postgres.ConnStr)
 	default:
 	default:
-		x, err = xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
+		sec.NewKey("connection_string", sqlutil.TestDB_Sqlite3.ConnStr)
 	}
 	}
 
 
-	x.DatabaseTZ = time.UTC
-	x.TZLocation = time.UTC
-
-	// x.ShowSQL()
-
+	// need to get engine to clean db before we init
+	engine, err := xorm.NewEngine(dbType, sec.Key("connection_string").String())
 	if err != nil {
 	if err != nil {
 		t.Fatalf("Failed to init test database: %v", err)
 		t.Fatalf("Failed to init test database: %v", err)
 	}
 	}
 
 
-	sqlutil.CleanDB(x)
+	dialect = migrator.NewDialect(engine)
+	if err := dialect.CleanDB(); err != nil {
+		t.Fatalf("Failed to clean test db %v", err)
+	}
 
 
-	if err := SetEngine(x); err != nil {
-		t.Fatal(err)
+	if err := sqlstore.Init(); err != nil {
+		t.Fatalf("Failed to init test database: %v", err)
 	}
 	}
 
 
-	return x
+	//// sqlstore.engine.DatabaseTZ = time.UTC
+	//// sqlstore.engine.TZLocation = time.UTC
+
+	return sqlstore
 }
 }
 
 
 func IsTestDbMySql() bool {
 func IsTestDbMySql() bool {
 	if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
 	if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
-		return db == dbMySql
+		return db == migrator.MYSQL
 	}
 	}
 
 
 	return false
 	return false
@@ -282,8 +293,20 @@ func IsTestDbMySql() bool {
 
 
 func IsTestDbPostgres() bool {
 func IsTestDbPostgres() bool {
 	if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
 	if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present {
-		return db == dbPostgres
+		return db == migrator.POSTGRES
 	}
 	}
 
 
 	return false
 	return false
 }
 }
+
+type DatabaseConfig struct {
+	Type, Host, Name, User, Pwd, Path, SslMode string
+	CaCertPath                                 string
+	ClientKeyPath                              string
+	ClientCertPath                             string
+	ServerCertName                             string
+	ConnectionString                           string
+	MaxOpenConn                                int
+	MaxIdleConn                                int
+	ConnMaxLifetime                            int
+}

+ 0 - 37
pkg/services/sqlstore/sqlutil/sqlutil.go

@@ -1,11 +1,5 @@
 package sqlutil
 package sqlutil
 
 
-import (
-	"fmt"
-
-	"github.com/go-xorm/xorm"
-)
-
 type TestDB struct {
 type TestDB struct {
 	DriverName string
 	DriverName string
 	ConnStr    string
 	ConnStr    string
@@ -15,34 +9,3 @@ var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:"}
 var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"}
 var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"}
 var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
 var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
 var TestDB_Mssql = TestDB{DriverName: "mssql", ConnStr: "server=localhost;port=1433;database=grafanatest;user id=grafana;password=Password!"}
 var TestDB_Mssql = TestDB{DriverName: "mssql", ConnStr: "server=localhost;port=1433;database=grafanatest;user id=grafana;password=Password!"}
-
-func CleanDB(x *xorm.Engine) {
-	if x.DriverName() == "postgres" {
-		sess := x.NewSession()
-		defer sess.Close()
-
-		if _, err := sess.Exec("DROP SCHEMA public CASCADE;"); err != nil {
-			panic("Failed to drop schema public")
-		}
-
-		if _, err := sess.Exec("CREATE SCHEMA public;"); err != nil {
-			panic("Failed to create schema public")
-		}
-	} else if x.DriverName() == "mysql" {
-		tables, _ := x.DBMetas()
-		sess := x.NewSession()
-		defer sess.Close()
-
-		for _, table := range tables {
-			if _, err := sess.Exec("set foreign_key_checks = 0"); err != nil {
-				panic("failed to disable foreign key checks")
-			}
-			if _, err := sess.Exec("drop table " + table.Name + " ;"); err != nil {
-				panic(fmt.Sprintf("failed to delete table: %v, err: %v", table.Name, err))
-			}
-			if _, err := sess.Exec("set foreign_key_checks = 1"); err != nil {
-				panic("failed to disable foreign key checks")
-			}
-		}
-	}
-}

+ 1 - 2
pkg/services/sqlstore/team.go

@@ -161,9 +161,8 @@ func SearchTeams(query *m.SearchTeamsQuery) error {
 	sql.WriteString(` order by team.name asc`)
 	sql.WriteString(` order by team.name asc`)
 
 
 	if query.Limit != 0 {
 	if query.Limit != 0 {
-		sql.WriteString(` limit ? offset ?`)
 		offset := query.Limit * (query.Page - 1)
 		offset := query.Limit * (query.Page - 1)
-		params = append(params, query.Limit, offset)
+		sql.WriteString(dialect.LimitOffset(int64(query.Limit), int64(offset)))
 	}
 	}
 
 
 	if err := x.Sql(sql.String(), params...).Find(&query.Result.Teams); err != nil {
 	if err := x.Sql(sql.String(), params...).Find(&query.Result.Teams); err != nil {

+ 8 - 2
pkg/services/sqlstore/user.go

@@ -60,8 +60,14 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error
 	org.Created = time.Now()
 	org.Created = time.Now()
 	org.Updated = time.Now()
 	org.Updated = time.Now()
 
 
-	if _, err := sess.Insert(&org); err != nil {
-		return 0, err
+	if org.Id != 0 {
+		if _, err := sess.InsertId(&org); err != nil {
+			return 0, err
+		}
+	} else {
+		if _, err := sess.InsertOne(&org); err != nil {
+			return 0, err
+		}
 	}
 	}
 
 
 	sess.publishAfterCommit(&events.OrgCreated{
 	sess.publishAfterCommit(&events.OrgCreated{

+ 22 - 9
pkg/setting/setting.go

@@ -52,12 +52,11 @@ var (
 	ApplicationName string
 	ApplicationName string
 
 
 	// Paths
 	// Paths
-	LogsPath         string
-	HomePath         string
-	DataPath         string
-	PluginsPath      string
-	ProvisioningPath string
-	CustomInitPath   = "conf/custom.ini"
+	LogsPath       string
+	HomePath       string
+	DataPath       string
+	PluginsPath    string
+	CustomInitPath = "conf/custom.ini"
 
 
 	// Log settings.
 	// Log settings.
 	LogModes   []string
 	LogModes   []string
@@ -125,6 +124,7 @@ var (
 	AuthProxyAutoSignUp     bool
 	AuthProxyAutoSignUp     bool
 	AuthProxyLdapSyncTtl    int
 	AuthProxyLdapSyncTtl    int
 	AuthProxyWhitelist      string
 	AuthProxyWhitelist      string
+	AuthProxyHeaders        map[string]string
 
 
 	// Basic Auth
 	// Basic Auth
 	BasicAuthEnabled bool
 	BasicAuthEnabled bool
@@ -187,6 +187,9 @@ var (
 type Cfg struct {
 type Cfg struct {
 	Raw *ini.File
 	Raw *ini.File
 
 
+	// Paths
+	ProvisioningPath string
+
 	// SMTP email settings
 	// SMTP email settings
 	Smtp SmtpSettings
 	Smtp SmtpSettings
 
 
@@ -492,7 +495,9 @@ func validateStaticRootPath() error {
 }
 }
 
 
 func NewCfg() *Cfg {
 func NewCfg() *Cfg {
-	return &Cfg{}
+	return &Cfg{
+		Raw: ini.Empty(),
+	}
 }
 }
 
 
 func (cfg *Cfg) Load(args *CommandLineArgs) error {
 func (cfg *Cfg) Load(args *CommandLineArgs) error {
@@ -516,7 +521,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	Env = iniFile.Section("").Key("app_mode").MustString("development")
 	Env = iniFile.Section("").Key("app_mode").MustString("development")
 	InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name")
 	InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name")
 	PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath)
 	PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath)
-	ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath)
+	cfg.ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath)
 	server := iniFile.Section("server")
 	server := iniFile.Section("server")
 	AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)
 	AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server)
 
 
@@ -611,6 +616,14 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
 	AuthProxyLdapSyncTtl = authProxy.Key("ldap_sync_ttl").MustInt()
 	AuthProxyLdapSyncTtl = authProxy.Key("ldap_sync_ttl").MustInt()
 	AuthProxyWhitelist = authProxy.Key("whitelist").String()
 	AuthProxyWhitelist = authProxy.Key("whitelist").String()
 
 
+	AuthProxyHeaders = make(map[string]string)
+	for _, propertyAndHeader := range util.SplitString(authProxy.Key("headers").String()) {
+		split := strings.SplitN(propertyAndHeader, ":", 2)
+		if len(split) == 2 {
+			AuthProxyHeaders[split[0]] = split[1]
+		}
+	}
+
 	// basic auth
 	// basic auth
 	authBasic := iniFile.Section("auth.basic")
 	authBasic := iniFile.Section("auth.basic")
 	BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
 	BasicAuthEnabled = authBasic.Key("enabled").MustBool(true)
@@ -719,6 +732,6 @@ func (cfg *Cfg) LogConfigSources() {
 	logger.Info("Path Data", "path", DataPath)
 	logger.Info("Path Data", "path", DataPath)
 	logger.Info("Path Logs", "path", LogsPath)
 	logger.Info("Path Logs", "path", LogsPath)
 	logger.Info("Path Plugins", "path", PluginsPath)
 	logger.Info("Path Plugins", "path", PluginsPath)
-	logger.Info("Path Provisioning", "path", ProvisioningPath)
+	logger.Info("Path Provisioning", "path", cfg.ProvisioningPath)
 	logger.Info("App mode " + Env)
 	logger.Info("App mode " + Env)
 }
 }

+ 55 - 43
pkg/tracing/tracing.go

@@ -1,68 +1,71 @@
 package tracing
 package tracing
 
 
 import (
 import (
+	"context"
 	"io"
 	"io"
 	"strings"
 	"strings"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 
 
 	opentracing "github.com/opentracing/opentracing-go"
 	opentracing "github.com/opentracing/opentracing-go"
 	jaegercfg "github.com/uber/jaeger-client-go/config"
 	jaegercfg "github.com/uber/jaeger-client-go/config"
-	ini "gopkg.in/ini.v1"
 )
 )
 
 
-var (
-	logger log.Logger = log.New("tracing")
-)
-
-type TracingSettings struct {
-	Enabled      bool
-	Address      string
-	CustomTags   map[string]string
-	SamplerType  string
-	SamplerParam float64
+func init() {
+	registry.RegisterService(&TracingService{})
 }
 }
 
 
-func Init(file *ini.File) (io.Closer, error) {
-	settings := parseSettings(file)
-	return internalInit(settings)
+type TracingService struct {
+	enabled      bool
+	address      string
+	customTags   map[string]string
+	samplerType  string
+	samplerParam float64
+	log          log.Logger
+	closer       io.Closer
+
+	Cfg *setting.Cfg `inject:""`
 }
 }
 
 
-func parseSettings(file *ini.File) *TracingSettings {
-	settings := &TracingSettings{}
+func (ts *TracingService) Init() error {
+	ts.log = log.New("tracing")
+	ts.parseSettings()
 
 
-	var section, err = setting.Raw.GetSection("tracing.jaeger")
-	if err != nil {
-		return settings
+	if ts.enabled {
+		ts.initGlobalTracer()
 	}
 	}
 
 
-	settings.Address = section.Key("address").MustString("")
-	if settings.Address != "" {
-		settings.Enabled = true
+	return nil
+}
+
+func (ts *TracingService) parseSettings() {
+	var section, err = ts.Cfg.Raw.GetSection("tracing.jaeger")
+	if err != nil {
+		return
 	}
 	}
 
 
-	settings.CustomTags = splitTagSettings(section.Key("always_included_tag").MustString(""))
-	settings.SamplerType = section.Key("sampler_type").MustString("")
-	settings.SamplerParam = section.Key("sampler_param").MustFloat64(1)
+	ts.address = section.Key("address").MustString("")
+	if ts.address != "" {
+		ts.enabled = true
+	}
 
 
-	return settings
+	ts.customTags = splitTagSettings(section.Key("always_included_tag").MustString(""))
+	ts.samplerType = section.Key("sampler_type").MustString("")
+	ts.samplerParam = section.Key("sampler_param").MustFloat64(1)
 }
 }
 
 
-func internalInit(settings *TracingSettings) (io.Closer, error) {
-	if !settings.Enabled {
-		return &nullCloser{}, nil
-	}
-
+func (ts *TracingService) initGlobalTracer() error {
 	cfg := jaegercfg.Configuration{
 	cfg := jaegercfg.Configuration{
-		Disabled: !settings.Enabled,
+		Disabled: !ts.enabled,
 		Sampler: &jaegercfg.SamplerConfig{
 		Sampler: &jaegercfg.SamplerConfig{
-			Type:  settings.SamplerType,
-			Param: settings.SamplerParam,
+			Type:  ts.samplerType,
+			Param: ts.samplerParam,
 		},
 		},
 		Reporter: &jaegercfg.ReporterConfig{
 		Reporter: &jaegercfg.ReporterConfig{
 			LogSpans:           false,
 			LogSpans:           false,
-			LocalAgentHostPort: settings.Address,
+			LocalAgentHostPort: ts.address,
 		},
 		},
 	}
 	}
 
 
@@ -71,18 +74,31 @@ func internalInit(settings *TracingSettings) (io.Closer, error) {
 	options := []jaegercfg.Option{}
 	options := []jaegercfg.Option{}
 	options = append(options, jaegercfg.Logger(jLogger))
 	options = append(options, jaegercfg.Logger(jLogger))
 
 
-	for tag, value := range settings.CustomTags {
+	for tag, value := range ts.customTags {
 		options = append(options, jaegercfg.Tag(tag, value))
 		options = append(options, jaegercfg.Tag(tag, value))
 	}
 	}
 
 
 	tracer, closer, err := cfg.New("grafana", options...)
 	tracer, closer, err := cfg.New("grafana", options...)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return err
 	}
 	}
 
 
 	opentracing.InitGlobalTracer(tracer)
 	opentracing.InitGlobalTracer(tracer)
-	logger.Info("Initializing Jaeger tracer", "address", settings.Address)
-	return closer, nil
+
+	ts.closer = closer
+
+	return nil
+}
+
+func (ts *TracingService) Run(ctx context.Context) error {
+	<-ctx.Done()
+
+	if ts.closer != nil {
+		ts.log.Info("Closing tracing")
+		ts.closer.Close()
+	}
+
+	return nil
 }
 }
 
 
 func splitTagSettings(input string) map[string]string {
 func splitTagSettings(input string) map[string]string {
@@ -110,7 +126,3 @@ func (jlw *jaegerLogWrapper) Error(msg string) {
 func (jlw *jaegerLogWrapper) Infof(msg string, args ...interface{}) {
 func (jlw *jaegerLogWrapper) Infof(msg string, args ...interface{}) {
 	jlw.logger.Info(msg, args)
 	jlw.logger.Info(msg, args)
 }
 }
-
-type nullCloser struct{}
-
-func (*nullCloser) Close() error { return nil }

+ 17 - 2
public/app/containers/Explore/Explore.tsx

@@ -10,6 +10,7 @@ import Graph from './Graph';
 import Table from './Table';
 import Table from './Table';
 import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
 import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
 import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
+import { decodePathComponent } from 'app/core/utils/location_util';
 
 
 function makeTimeSeriesList(dataList, options) {
 function makeTimeSeriesList(dataList, options) {
   return dataList.map((seriesData, index) => {
   return dataList.map((seriesData, index) => {
@@ -38,6 +39,19 @@ function makeTimeSeriesList(dataList, options) {
   });
   });
 }
 }
 
 
+function parseInitialQueries(initial) {
+  if (!initial) {
+    return [];
+  }
+  try {
+    const parsed = JSON.parse(decodePathComponent(initial));
+    return parsed.queries.map(q => q.query);
+  } catch (e) {
+    console.error(e);
+    return [];
+  }
+}
+
 interface IExploreState {
 interface IExploreState {
   datasource: any;
   datasource: any;
   datasourceError: any;
   datasourceError: any;
@@ -58,6 +72,7 @@ export class Explore extends React.Component<any, IExploreState> {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
+    const initialQueries = parseInitialQueries(props.routeParams.initial);
     this.state = {
     this.state = {
       datasource: null,
       datasource: null,
       datasourceError: null,
       datasourceError: null,
@@ -65,7 +80,7 @@ export class Explore extends React.Component<any, IExploreState> {
       graphResult: null,
       graphResult: null,
       latency: 0,
       latency: 0,
       loading: false,
       loading: false,
-      queries: ensureQueries(),
+      queries: ensureQueries(initialQueries),
       requestOptions: null,
       requestOptions: null,
       showingGraph: true,
       showingGraph: true,
       showingTable: true,
       showingTable: true,
@@ -77,7 +92,7 @@ export class Explore extends React.Component<any, IExploreState> {
     const datasource = await this.props.datasourceSrv.get();
     const datasource = await this.props.datasourceSrv.get();
     const testResult = await datasource.testDatasource();
     const testResult = await datasource.testDatasource();
     if (testResult.status === 'success') {
     if (testResult.status === 'success') {
-      this.setState({ datasource, datasourceError: null, datasourceLoading: false });
+      this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
     } else {
     } else {
       this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
       this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false });
     }
     }

+ 15 - 4
public/app/containers/Explore/QueryRows.tsx

@@ -6,13 +6,16 @@ class QueryRow extends PureComponent<any, any> {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
     this.state = {
     this.state = {
-      query: '',
+      edited: false,
+      query: props.query || '',
     };
     };
   }
   }
 
 
   handleChangeQuery = value => {
   handleChangeQuery = value => {
     const { index, onChangeQuery } = this.props;
     const { index, onChangeQuery } = this.props;
-    this.setState({ query: value });
+    const { query } = this.state;
+    const edited = query !== value;
+    this.setState({ edited, query: value });
     if (onChangeQuery) {
     if (onChangeQuery) {
       onChangeQuery(value, index);
       onChangeQuery(value, index);
     }
     }
@@ -41,6 +44,7 @@ class QueryRow extends PureComponent<any, any> {
 
 
   render() {
   render() {
     const { request } = this.props;
     const { request } = this.props;
+    const { edited, query } = this.state;
     return (
     return (
       <div className="query-row">
       <div className="query-row">
         <div className="query-row-tools">
         <div className="query-row-tools">
@@ -52,7 +56,12 @@ class QueryRow extends PureComponent<any, any> {
           </button>
           </button>
         </div>
         </div>
         <div className="query-field-wrapper">
         <div className="query-field-wrapper">
-          <QueryField onPressEnter={this.handlePressEnter} onQueryChange={this.handleChangeQuery} request={request} />
+          <QueryField
+            initialQuery={edited ? null : query}
+            onPressEnter={this.handlePressEnter}
+            onQueryChange={this.handleChangeQuery}
+            request={request}
+          />
         </div>
         </div>
       </div>
       </div>
     );
     );
@@ -63,7 +72,9 @@ export default class QueryRows extends PureComponent<any, any> {
   render() {
   render() {
     const { className = '', queries, ...handlers } = this.props;
     const { className = '', queries, ...handlers } = this.props;
     return (
     return (
-      <div className={className}>{queries.map((q, index) => <QueryRow key={q.key} index={index} {...handlers} />)}</div>
+      <div className={className}>
+        {queries.map((q, index) => <QueryRow key={q.key} index={index} query={q.query} {...handlers} />)}
+      </div>
     );
     );
   }
   }
 }
 }

+ 1 - 0
public/app/core/services/analytics.ts

@@ -19,6 +19,7 @@ export class Analytics {
       });
       });
     ga.l = +new Date();
     ga.l = +new Date();
     ga('create', (<any>config).googleAnalyticsId, 'auto');
     ga('create', (<any>config).googleAnalyticsId, 'auto');
+    ga('set', 'anonymizeIp', true);
     return ga;
     return ga;
   }
   }
 
 

+ 13 - 1
public/app/core/services/keybindingSrv.ts

@@ -3,6 +3,7 @@ import _ from 'lodash';
 
 
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
+import { encodePathComponent } from 'app/core/utils/location_util';
 
 
 import Mousetrap from 'mousetrap';
 import Mousetrap from 'mousetrap';
 import 'mousetrap-global-bind';
 import 'mousetrap-global-bind';
@@ -13,7 +14,7 @@ export class KeybindingSrv {
   timepickerOpen = false;
   timepickerOpen = false;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor(private $rootScope, private $location) {
+  constructor(private $rootScope, private $location, private datasourceSrv) {
     // clear out all shortcuts on route change
     // clear out all shortcuts on route change
     $rootScope.$on('$routeChangeSuccess', () => {
     $rootScope.$on('$routeChangeSuccess', () => {
       Mousetrap.reset();
       Mousetrap.reset();
@@ -176,6 +177,17 @@ export class KeybindingSrv {
       }
       }
     });
     });
 
 
+    this.bind('x', async () => {
+      if (dashboard.meta.focusPanelId) {
+        const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
+        const datasource = await this.datasourceSrv.get(panel.datasource);
+        if (datasource && datasource.supportsExplore) {
+          const exploreState = encodePathComponent(JSON.stringify(datasource.getExploreState(panel)));
+          this.$location.url(`/explore/${exploreState}`);
+        }
+      }
+    });
+
     // delete panel
     // delete panel
     this.bind('p r', () => {
     this.bind('p r', () => {
       if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {
       if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) {

+ 79 - 18
public/app/core/specs/file_export.jest.ts

@@ -30,17 +30,17 @@ describe('file_export', () => {
     it('should export points in proper order', () => {
     it('should export points in proper order', () => {
       let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat);
       let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat);
       const expectedText =
       const expectedText =
-        'Series;Time;Value\n' +
-        'series_1;1500026100;1\n' +
-        'series_1;1500026200;2\n' +
-        'series_1;1500026300;null\n' +
-        'series_1;1500026400;null\n' +
-        'series_1;1500026500;null\n' +
-        'series_1;1500026600;6\n' +
-        'series_2;1500026100;11\n' +
-        'series_2;1500026200;12\n' +
-        'series_2;1500026300;13\n' +
-        'series_2;1500026500;15\n';
+        '"Series";"Time";"Value"\r\n' +
+        '"series_1";"1500026100";1\r\n' +
+        '"series_1";"1500026200";2\r\n' +
+        '"series_1";"1500026300";null\r\n' +
+        '"series_1";"1500026400";null\r\n' +
+        '"series_1";"1500026500";null\r\n' +
+        '"series_1";"1500026600";6\r\n' +
+        '"series_2";"1500026100";11\r\n' +
+        '"series_2";"1500026200";12\r\n' +
+        '"series_2";"1500026300";13\r\n' +
+        '"series_2";"1500026500";15';
 
 
       expect(text).toBe(expectedText);
       expect(text).toBe(expectedText);
     });
     });
@@ -50,15 +50,76 @@ describe('file_export', () => {
     it('should export points in proper order', () => {
     it('should export points in proper order', () => {
       let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat);
       let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat);
       const expectedText =
       const expectedText =
-        'Time;series_1;series_2\n' +
-        '1500026100;1;11\n' +
-        '1500026200;2;12\n' +
-        '1500026300;null;13\n' +
-        '1500026400;null;null\n' +
-        '1500026500;null;15\n' +
-        '1500026600;6;null\n';
+        '"Time";"series_1";"series_2"\r\n' +
+        '"1500026100";1;11\r\n' +
+        '"1500026200";2;12\r\n' +
+        '"1500026300";null;13\r\n' +
+        '"1500026400";null;null\r\n' +
+        '"1500026500";null;15\r\n' +
+        '"1500026600";6;null';
 
 
       expect(text).toBe(expectedText);
       expect(text).toBe(expectedText);
     });
     });
   });
   });
+
+  describe('when exporting table data to csv', () => {
+    it('should properly escape special characters and quote all string values', () => {
+      const inputTable = {
+        columns: [
+          { title: 'integer_value' },
+          { text: 'string_value' },
+          { title: 'float_value' },
+          { text: 'boolean_value' },
+        ],
+        rows: [
+          [123, 'some_string', 1.234, true],
+          [0o765, 'some string with " in the middle', 1e-2, false],
+          [0o765, 'some string with "" in the middle', 1e-2, false],
+          [0o765, 'some string with """ in the middle', 1e-2, false],
+          [0o765, '"some string with " at the beginning', 1e-2, false],
+          [0o765, 'some string with " at the end"', 1e-2, false],
+          [0x123, 'some string with \n in the middle', 10.01, false],
+          [0b1011, 'some string with ; in the middle', -12.34, true],
+          [123, 'some string with ;; in the middle', -12.34, true],
+        ],
+      };
+
+      const returnedText = fileExport.convertTableDataToCsv(inputTable, false);
+
+      const expectedText =
+        '"integer_value";"string_value";"float_value";"boolean_value"\r\n' +
+        '123;"some_string";1.234;true\r\n' +
+        '501;"some string with "" in the middle";0.01;false\r\n' +
+        '501;"some string with """" in the middle";0.01;false\r\n' +
+        '501;"some string with """""" in the middle";0.01;false\r\n' +
+        '501;"""some string with "" at the beginning";0.01;false\r\n' +
+        '501;"some string with "" at the end""";0.01;false\r\n' +
+        '291;"some string with \n in the middle";10.01;false\r\n' +
+        '11;"some string with ; in the middle";-12.34;true\r\n' +
+        '123;"some string with ;; in the middle";-12.34;true';
+
+      expect(returnedText).toBe(expectedText);
+    });
+
+    it('should decode HTML encoded characters', function() {
+      const inputTable = {
+        columns: [{ text: 'string_value' }],
+        rows: [
+          ['&quot;&amp;&auml;'],
+          ['<strong>&quot;some html&quot;</strong>'],
+          ['<a href="http://something/index.html">some text</a>'],
+        ],
+      };
+
+      const returnedText = fileExport.convertTableDataToCsv(inputTable, false);
+
+      const expectedText =
+        '"string_value"\r\n' +
+        '"""&ä"\r\n' +
+        '"<strong>""some html""</strong>"\r\n' +
+        '"<a href=""http://something/index.html"">some text</a>"';
+
+      expect(returnedText).toBe(expectedText);
+    });
+  });
 });
 });

Некоторые файлы не были показаны из-за большого количества измененных файлов