mirror of https://github.com/grafana/grafana
commit
f14b45bc86
@ -1,31 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- /docs/grafana/latest/linking/ |
||||
title: Linking |
||||
weight: 120 |
||||
--- |
||||
|
||||
# Linking overview |
||||
|
||||
You can use links to navigate between commonly-used dashboards or to connect others to your visualizations. Links let you create shortcuts to other dashboards, panels, and even external websites. |
||||
|
||||
Grafana supports dashboard links, panel links, and data links. Dashboard links are displayed at the top of the dashboard. Panel links are accessible by clicking an icon on the top left corner of the panel. |
||||
|
||||
## Which link should you use? |
||||
|
||||
Start by figuring out how you're currently navigating between dashboards. If you're often jumping between a set of dashboards and struggling to find the same context in each, links can help optimize your workflow. |
||||
|
||||
The next step is to figure out which link type is right for your workflow. Even though all the link types in Grafana are used to create shortcuts to other dashboards or external websites, they work in different contexts. |
||||
|
||||
- If the link relates to most if not all of the panels in the dashboard, use [dashboard links]({{< relref "dashboard-links/" >}}). |
||||
- If you want to drill down into specific panels, use [panel links]({{< relref "panel-links/" >}}). |
||||
- If you want to link to an external site, you can use either a dashboard link or a panel link. |
||||
- If you want to drill down into a specific series, or even a single measurement, use [data links]({{< relref "data-links/" >}}). |
||||
|
||||
## Controlling time range using the URL |
||||
|
||||
You can control the time range of a panel or dashboard by providing following query parameters in dashboard URL: |
||||
|
||||
- `from` - defines lower limit of the time range, specified in ms epoch |
||||
- `to` - defines upper limit of the time range, specified in ms epoch |
||||
- `time` and `time.window` - defines a time range from `time-time.window/2` to `time+time.window/2`. Both params should be specified in ms. For example `?time=1500000000000&time.window=10000` will result in 10s time range from 1499999995000 to 1500000005000 |
@ -1,62 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- /docs/grafana/latest/linking/data-link-variables/ |
||||
- /docs/grafana/latest/variables/url-variables/ |
||||
- /docs/grafana/latest/variables/variable-types/url-variables/ |
||||
keywords: |
||||
- grafana |
||||
- url variables |
||||
- documentation |
||||
- variables |
||||
- data link |
||||
title: URL variables |
||||
weight: 400 |
||||
--- |
||||
|
||||
# Data link variables |
||||
|
||||
You can use variables in data links to refer to series fields, labels, and values. For more information about data links, refer to [Data links]({{< relref "data-links/" >}}). |
||||
|
||||
To see a list of available variables, type `$` in the data link **URL** field to see a list of variables that you can use. |
||||
|
||||
> **Note:** These variables changed in 6.4 so if you have an older version of Grafana, then use the version picker to select docs for an older version of Grafana. |
||||
|
||||
You can also use template variables in your data links URLs, refer to [Templates and variables]({{< relref "../variables/" >}}) for more information on template variables. |
||||
|
||||
## Time range panel variables |
||||
|
||||
These variables allow you to include the current time range in the data link URL. |
||||
|
||||
- `__url_time_range` - current dashboard's time range (i.e. `?from=now-6h&to=now`) |
||||
- `$__from and $__to` - For more information, refer to [Global variables]({{< relref "../variables/variable-types/global-variables/#__from-and-__to" >}}). |
||||
|
||||
## Series variables |
||||
|
||||
Series specific variables are available under `__series` namespace: |
||||
|
||||
- `__series.name` - series name to the URL |
||||
|
||||
## Field variables |
||||
|
||||
Field-specific variables are available under `__field` namespace: |
||||
|
||||
- `__field.name` - the name of the field |
||||
- `__field.labels.<LABEL>` - label's value to the URL. If your label contains dots, then use `__field.labels["<LABEL>"]` syntax. |
||||
|
||||
## Value variables |
||||
|
||||
Value-specific variables are available under `__value` namespace: |
||||
|
||||
- `__value.time` - value's timestamp (Unix ms epoch) to the URL (i.e. `?time=1560268814105`) |
||||
- `__value.raw` - raw value |
||||
- `__value.numeric` - numeric representation of a value |
||||
- `__value.text` - text representation of a value |
||||
- `__value.calc` - calculation name if the value is result of calculation |
||||
|
||||
## Template variables |
||||
|
||||
When linking to another dashboard that uses template variables, select variable values for whoever clicks the link. |
||||
|
||||
`${myvar:queryparam}` - where `myvar` is a name of the template variable that matches one in the current dashboard that you want to use. |
||||
|
||||
If you want to add all of the current dashboard's variables to the URL, then use `__all_variables`. |
@ -1,54 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- /docs/grafana/latest/linking/data-links/ |
||||
- /docs/grafana/latest/reference/datalinks/ |
||||
keywords: |
||||
- grafana |
||||
- data links |
||||
- documentation |
||||
- playlist |
||||
title: Data links |
||||
--- |
||||
|
||||
# Data links |
||||
|
||||
Data links allow you to provide more granular context to your links. You can create links that include the series name or even the value under the cursor. For example, if your visualization showed four servers, you could add a data link to one or two of them. |
||||
|
||||
The link itself is accessible in different ways depending on the visualization. For the Graph you need to click on a data point or line, for a panel like |
||||
Stat, Gauge, or Bar Gauge you can click anywhere on the visualization to open the context menu. |
||||
|
||||
You can use variables in data links to send people to a detailed dashboard with preserved data filters. For example, you could use variables to specify a time range, series, and variable selection. For more information, refer to [Data link variables]({{< relref "data-link-variables/" >}}). |
||||
|
||||
## Typeahead suggestions |
||||
|
||||
When creating or updating a data link, press Cmd+Space or Ctrl+Space on your keyboard to open the typeahead suggestions to more easily add variables to your URL. |
||||
|
||||
{{< figure src="/static/img/docs/data_link_typeahead.png" max-width= "800px" >}} |
||||
|
||||
## Add a data link |
||||
|
||||
1. Hover your cursor over the panel that you want to add a link to and then press `e`. Or click the dropdown arrow next to the panel title and then click **Edit**. |
||||
1. On the Field tab, scroll down to the Data links section. |
||||
1. Expand Data links and then click **Add link**. |
||||
1. Enter a **Title**. **Title** is a human-readable label for the link that will be displayed in the UI. |
||||
1. Enter the **URL** you want to link to. |
||||
|
||||
You can even add one of the template variables defined in the dashboard. Click in the **URL** field and then type `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. By adding template variables to your panel link, the link sends the user to the right context, with the relevant variables already set. For more information, refer to [Data link variables]({{< relref "data-link-variables/" >}}). |
||||
|
||||
1. If you want the link to open in a new tab, then select **Open in a new tab**. |
||||
1. Click **Save** to save changes and close the window. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
||||
|
||||
## Update a data link |
||||
|
||||
1. On the Field tab, find the link that you want to make changes to. |
||||
1. Click the Edit (pencil) icon to open the Edit link window. |
||||
1. Make any necessary changes. |
||||
1. Click **Save** to save changes and close the window. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
||||
|
||||
## Delete a data link |
||||
|
||||
1. On the Field tab, find the link that you want to delete. |
||||
1. Click the **X** icon next to the link you want to delete. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
@ -1,39 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- /docs/grafana/latest/features/navigation-links/ |
||||
- /docs/grafana/latest/linking/linking-overview/ |
||||
keywords: |
||||
- grafana |
||||
- linking |
||||
- create links |
||||
- link panels |
||||
- link dashboards |
||||
- navigate |
||||
title: Linking overview |
||||
weight: 100 |
||||
--- |
||||
|
||||
# Linking overview |
||||
|
||||
You can use links to navigate between commonly-used dashboards or to connect others to your visualizations. Links let you create shortcuts to other dashboards, panels, and even external websites. |
||||
|
||||
Grafana supports dashboard links, panel links, and data links. Dashboard links are displayed at the top of the dashboard. Panel links are accessible by clicking an icon on the top left corner of the panel. |
||||
|
||||
## Which link should you use? |
||||
|
||||
Start by figuring out how you're currently navigating between dashboards. If you're often jumping between a set of dashboards and struggling to find the same context in each, links can help optimize your workflow. |
||||
|
||||
The next step is to figure out which link type is right for your workflow. Even though all the link types in Grafana are used to create shortcuts to other dashboards or external websites, they work in different contexts. |
||||
|
||||
- If the link relates to most if not all of the panels in the dashboard, use [dashboard links]({{< relref "dashboard-links/" >}}). |
||||
- If you want to drill down into specific panels, use [panel links]({{< relref "panel-links/" >}}). |
||||
- If you want to link to an external site, you can use either a dashboard link or a panel link. |
||||
- If you want to drill down into a specific series, or even a single measurement, use [data links]({{< relref "data-links/" >}}). |
||||
|
||||
## Controlling time range using the URL |
||||
|
||||
You can control the time range of a panel or dashboard by providing following query parameters in dashboard URL: |
||||
|
||||
- `from` - defines lower limit of the time range, specified in ms epoch |
||||
- `to` - defines upper limit of the time range, specified in ms epoch |
||||
- `time` and `time.window` - defines a time range from `time-time.window/2` to `time+time.window/2`. Both params should be specified in ms. For example `?time=1500000000000&time.window=10000` will result in 10s time range from 1499999995000 to 1500000005000 |
@ -1,52 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- /docs/grafana/latest/features/navigation-links/ |
||||
- /docs/grafana/latest/linking/panel-links/ |
||||
description: '' |
||||
keywords: |
||||
- grafana |
||||
- linking |
||||
- create links |
||||
- link panels |
||||
- link dashboards |
||||
- navigate |
||||
title: Panel links |
||||
weight: 300 |
||||
--- |
||||
|
||||
# Panel links |
||||
|
||||
{{< docs/shared "panels/panel-links-intro.md" >}} |
||||
|
||||
Click the icon on the top left corner of a panel to see available panel links. |
||||
|
||||
<img class="no-shadow" src="/static/img/docs/linking/panel-links.png" width="500px"> |
||||
|
||||
## Add a panel link |
||||
|
||||
1. Hover your cursor over the panel that you want to add a link to and then press `e`. Or click the dropdown arrow next to the panel title and then click **Edit**. |
||||
1. On the Panel tab, scroll down to the Links section. |
||||
1. Expand Links and then click **Add link**. |
||||
1. Enter a **Title**. **Title** is a human-readable label for the link that will be displayed in the UI. |
||||
1. Enter the **URL** you want to link to. |
||||
You can even add one of the template variables defined in the dashboard. Press Ctrl+Space or Cmd+Space and click in the **URL** field to see the available variables. By adding template variables to your panel link, the link sends the user to the right context, with the relevant variables already set. You can also use time variables: |
||||
- `from` - Defines the lower limit of the time range, specified in ms epoch. |
||||
- `to` - Defines the upper limit of the time range, specified in ms epoch. |
||||
- `time` and `time.window` - Define a time range from `time-time.window/2` to `time+time.window/2`. Both params should be specified in ms. For example `?time=1500000000000&time.window=10000` will result in 10s time range from 1499999995000 to 1500000005000. |
||||
1. If you want the link to open in a new tab, then select **Open in a new tab**. |
||||
1. Click **Save** to save changes and close the window. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
||||
|
||||
## Update a panel link |
||||
|
||||
1. On the Panel tab, find the link that you want to make changes to. |
||||
1. Click the Edit (pencil) icon to open the Edit link window. |
||||
1. Make any necessary changes. |
||||
1. Click **Save** to save changes and close the window. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
||||
|
||||
## Delete a panel link |
||||
|
||||
1. On the Panel tab, find the link that you want to delete. |
||||
1. Click the **X** icon next to the link you want to delete. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
@ -0,0 +1,114 @@ |
||||
--- |
||||
aliases: |
||||
- /docs/grafana/latest/linking/data-link-variables/ |
||||
- /docs/grafana/latest/variables/url-variables/ |
||||
- /docs/grafana/latest/variables/variable-types/url-variables/ |
||||
- /docs/grafana/latest/linking/data-links/ |
||||
- /docs/grafana/latest/reference/datalinks/ |
||||
- /docs/grafana/latest/panels/configure-data-links/ |
||||
keywords: |
||||
- grafana |
||||
- url variables |
||||
- variables |
||||
- data link |
||||
- documentation |
||||
- playlist |
||||
title: Configure data links |
||||
menuTitle: Configure data links |
||||
weight: 400 |
||||
--- |
||||
|
||||
# Configure data links |
||||
|
||||
You can use data link variables or data links to create links between panels. |
||||
|
||||
## Data link variables |
||||
|
||||
You can use variables in data links to refer to series fields, labels, and values. For more information about data links, refer to [Data links]({{< relref "#data-links" >}}). |
||||
|
||||
To see a list of available variables, type `$` in the data link **URL** field to see a list of variables that you can use. |
||||
|
||||
> **Note:** These variables changed in 6.4 so if you have an older version of Grafana, then use the version picker to select docs for an older version of Grafana. |
||||
|
||||
You can also use template variables in your data links URLs, refer to [Templates and variables]({{< relref "../../variables/" >}}) for more information on template variables. |
||||
|
||||
## Time range panel variables |
||||
|
||||
These variables allow you to include the current time range in the data link URL. |
||||
|
||||
- `__url_time_range` - current dashboard's time range (i.e. `?from=now-6h&to=now`) |
||||
- `$__from and $__to` - For more information, refer to [Global variables]({{< relref "../../variables/variable-types/global-variables/#__from-and-__to" >}}). |
||||
|
||||
## Series variables |
||||
|
||||
Series specific variables are available under `__series` namespace: |
||||
|
||||
- `__series.name` - series name to the URL |
||||
|
||||
## Field variables |
||||
|
||||
Field-specific variables are available under `__field` namespace: |
||||
|
||||
- `__field.name` - the name of the field |
||||
- `__field.labels.<LABEL>` - label's value to the URL. If your label contains dots, then use `__field.labels["<LABEL>"]` syntax. |
||||
|
||||
## Value variables |
||||
|
||||
Value-specific variables are available under `__value` namespace: |
||||
|
||||
- `__value.time` - value's timestamp (Unix ms epoch) to the URL (i.e. `?time=1560268814105`) |
||||
- `__value.raw` - raw value |
||||
- `__value.numeric` - numeric representation of a value |
||||
- `__value.text` - text representation of a value |
||||
- `__value.calc` - calculation name if the value is result of calculation |
||||
|
||||
## Template variables |
||||
|
||||
When linking to another dashboard that uses template variables, select variable values for whoever clicks the link. |
||||
|
||||
`${myvar:queryparam}` - where `myvar` is a name of the template variable that matches one in the current dashboard that you want to use. |
||||
|
||||
If you want to add all of the current dashboard's variables to the URL, then use `__all_variables`. |
||||
|
||||
## Data links |
||||
|
||||
Data links allow you to provide more granular context to your links. You can create links that include the series name or even the value under the cursor. For example, if your visualization showed four servers, you could add a data link to one or two of them. |
||||
|
||||
The link itself is accessible in different ways depending on the visualization. For the Graph you need to click on a data point or line, for a panel like |
||||
Stat, Gauge, or Bar Gauge you can click anywhere on the visualization to open the context menu. |
||||
|
||||
You can use variables in data links to send people to a detailed dashboard with preserved data filters. For example, you could use variables to specify a time range, series, and variable selection. For more information, refer to [Data link variables]({{< relref "#data-link-variables" >}}). |
||||
|
||||
### Typeahead suggestions |
||||
|
||||
When creating or updating a data link, press Cmd+Space or Ctrl+Space on your keyboard to open the typeahead suggestions to more easily add variables to your URL. |
||||
|
||||
{{< figure src="/static/img/docs/data_link_typeahead.png" max-width= "800px" >}} |
||||
|
||||
### Add a data link |
||||
|
||||
1. Hover your cursor over the panel that you want to add a link to and then press `e`. Or click the dropdown arrow next to the panel title and then click **Edit**. |
||||
1. On the Field tab, scroll down to the Data links section. |
||||
1. Expand Data links and then click **Add link**. |
||||
1. Enter a **Title**. **Title** is a human-readable label for the link that will be displayed in the UI. |
||||
1. Enter the **URL** you want to link to. |
||||
|
||||
You can even add one of the template variables defined in the dashboard. Click in the **URL** field and then type `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. By adding template variables to your panel link, the link sends the user to the right context, with the relevant variables already set. For more information, refer to [Data link variables]({{< relref "#data-link-variables" >}}). |
||||
|
||||
1. If you want the link to open in a new tab, then select **Open in a new tab**. |
||||
1. Click **Save** to save changes and close the window. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
||||
|
||||
### Update a data link |
||||
|
||||
1. On the Field tab, find the link that you want to make changes to. |
||||
1. Click the Edit (pencil) icon to open the Edit link window. |
||||
1. Make any necessary changes. |
||||
1. Click **Save** to save changes and close the window. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
||||
|
||||
### Delete a data link |
||||
|
||||
1. On the Field tab, find the link that you want to delete. |
||||
1. Click the **X** icon next to the link you want to delete. |
||||
1. Click **Save** in the upper right to save your changes to the dashboard. |
@ -1,13 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- /docs/grafana/latest/panels/working-with-panels/add-link-to-panel/ |
||||
- /docs/sources/panels/working-with-panels/add-link-to-panel/ |
||||
title: Add a link to a panel |
||||
weight: 60 |
||||
--- |
||||
|
||||
# Add a link to a panel |
||||
|
||||
{{< docs/shared "panels/panel-links-intro.md" >}} |
||||
|
||||
For more information, refer to [Panel links]({{< relref "../../linking/panel-links/" >}}). |
@ -1,48 +0,0 @@ |
||||
--- |
||||
aliases: |
||||
- /docs/grafana/latest/features/panels/table_panel/ |
||||
- /docs/grafana/latest/panels/visualizations/table/filter-table-columns/ |
||||
- /docs/grafana/latest/reference/table/ |
||||
- /docs/grafana/latest/visualizations/table/filter-table-columns/ |
||||
- /docs/grafana/next/panels/visualizations/table/table-field-options/ |
||||
keywords: |
||||
- grafana |
||||
- table options |
||||
- documentation |
||||
- format tables |
||||
- table filter |
||||
- filter columns |
||||
title: Filter table columns |
||||
weight: 600 |
||||
--- |
||||
|
||||
# Filter table columns |
||||
|
||||
If you turn on the **Column filter**, then you can filter table options. |
||||
|
||||
## Turn on column filtering |
||||
|
||||
1. In Grafana, navigate to the dashboard with the table with the columns that you want to filter. |
||||
1. On the table panel you want to filter, open the panel editor. |
||||
1. Click the **Field** tab. |
||||
1. In Table options, turn on the **Column filter** option. |
||||
|
||||
A filter icon appears next to each column title. |
||||
|
||||
{{< figure src="/static/img/docs/tables/column-filter-with-icon.png" max-width="500px" caption="Column filtering turned on" class="docs-image--no-shadow" >}} |
||||
|
||||
## Filter column values |
||||
|
||||
To filter column values, click the filter (funnel) icon next to a column title. Grafana displays the filter options for that column. |
||||
|
||||
{{< figure src="/static/img/docs/tables/filter-column-values.png" max-width="500px" caption="Filter column values" class="docs-image--no-shadow" >}} |
||||
|
||||
Click the check box next to the values that you want to display. Enter text in the search field at the top to show those values in the display so that you can select them rather than scroll to find them. |
||||
|
||||
## Clear column filters |
||||
|
||||
Columns with filters applied have a blue funnel displayed next to the title. |
||||
|
||||
{{< figure src="/static/img/docs/tables/filtered-column.png" max-width="500px" caption="Filtered column" class="docs-image--no-shadow" >}} |
||||
|
||||
To remove the filter, click the blue funnel icon and then click **Clear filter**. |
@ -0,0 +1,177 @@ |
||||
package definitions |
||||
|
||||
import ( |
||||
"github.com/grafana/grafana/pkg/api/dtos" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
) |
||||
|
||||
// swagger:route GET /playlists playlists searchPlaylists
|
||||
//
|
||||
// Get playlists.
|
||||
//
|
||||
// Responses:
|
||||
// 200: searchPlaylistsResponse
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:route GET /playlists/{uid} playlists getPlaylist
|
||||
//
|
||||
// Get playlist by UID.
|
||||
//
|
||||
// Responses:
|
||||
// 200: getPlaylistResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:route GET /playlists/{uid}/items playlists getPlaylistItems
|
||||
//
|
||||
// Get playlist items.
|
||||
//
|
||||
// Responses:
|
||||
// 200: getPlaylistItemsResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:route GET /playlists/{uid}/dashboards playlists getPlaylistDashboards
|
||||
//
|
||||
// Get playlist dashboards.
|
||||
//
|
||||
// Responses:
|
||||
// 200: getPlaylistDashboardsResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:route DELETE /playlists/{uid} playlists deletePlaylist
|
||||
//
|
||||
// Delete pllaylist.
|
||||
//
|
||||
// Responses:
|
||||
// 200: okResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:route PUT /playlists/{uid} playlists updatePlaylist
|
||||
//
|
||||
// Update playlist.
|
||||
//
|
||||
// Responses:
|
||||
// 200: updatePlaylistResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:route POST /playlists playlists createPlaylist
|
||||
//
|
||||
// Create playlist.
|
||||
//
|
||||
// Responses:
|
||||
// 200: createPlaylistResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:parameters searchPlaylists
|
||||
type SearchPlaylistsParams struct { |
||||
// in:query
|
||||
// required:false
|
||||
Query string `json:"query"` |
||||
// in:limit
|
||||
// required:false
|
||||
Limit int `json:"limit"` |
||||
} |
||||
|
||||
// swagger:parameters getPlaylist
|
||||
type GetPlaylistParams struct { |
||||
// in:path
|
||||
// required:true
|
||||
UID string `json:"uid"` |
||||
} |
||||
|
||||
// swagger:parameters getPlaylistItems
|
||||
type GetPlaylistItemsParams struct { |
||||
// in:path
|
||||
// required:true
|
||||
UID string `json:"uid"` |
||||
} |
||||
|
||||
// swagger:parameters getPlaylistDashboards
|
||||
type GetPlaylistDashboardsParams struct { |
||||
// in:path
|
||||
// required:true
|
||||
UID string `json:"uid"` |
||||
} |
||||
|
||||
// swagger:parameters deletePlaylist
|
||||
type DeletePlaylistParams struct { |
||||
// in:path
|
||||
// required:true
|
||||
UID string `json:"uid"` |
||||
} |
||||
|
||||
// swagger:parameters updatePlaylist
|
||||
type UpdatePlaylistParams struct { |
||||
// in:body
|
||||
// required:true
|
||||
Body models.UpdatePlaylistCommand |
||||
// in:path
|
||||
// required:true
|
||||
UID string `json:"uid"` |
||||
} |
||||
|
||||
// swagger:parameters createPlaylist
|
||||
type CreatePlaylistParams struct { |
||||
// in:body
|
||||
// required:true
|
||||
Body models.CreatePlaylistCommand |
||||
} |
||||
|
||||
// swagger:response searchPlaylistsResponse
|
||||
type SearchPlaylistsResponse struct { |
||||
// The response message
|
||||
// in: body
|
||||
Body models.Playlists `json:"body"` |
||||
} |
||||
|
||||
// swagger:response getPlaylistResponse
|
||||
type GetPlaylistResponse struct { |
||||
// The response message
|
||||
// in: body
|
||||
Body *models.PlaylistDTO `json:"body"` |
||||
} |
||||
|
||||
// swagger:response getPlaylistItemsResponse
|
||||
type GetPlaylistItemsResponse struct { |
||||
// The response message
|
||||
// in: body
|
||||
Body []models.PlaylistItemDTO `json:"body"` |
||||
} |
||||
|
||||
// swagger:response getPlaylistDashboardsResponse
|
||||
type GetPlaylistDashboardsResponse struct { |
||||
// The response message
|
||||
// in: body
|
||||
Body dtos.PlaylistDashboardsSlice `json:"body"` |
||||
} |
||||
|
||||
// swagger:response updatePlaylistResponse
|
||||
type UpdatePlaylistResponseResponse struct { |
||||
// The response message
|
||||
// in: body
|
||||
Body *models.PlaylistDTO `json:"body"` |
||||
} |
||||
|
||||
// swagger:response createPlaylistResponse
|
||||
type CreatePlaylistResponse struct { |
||||
// The response message
|
||||
// in: body
|
||||
Body *models.Playlist `json:"body"` |
||||
} |
@ -0,0 +1,127 @@ |
||||
package response |
||||
|
||||
import ( |
||||
"errors" |
||||
"net/http" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/util/errutil" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestErrors(t *testing.T) { |
||||
const fakeNotFoundMessage = "I looked, but did not find the thing" |
||||
const genericErrorMessage = "Something went wrong in parsing the request" |
||||
|
||||
cases := []struct { |
||||
name string |
||||
|
||||
// inputs
|
||||
err error |
||||
statusCode int |
||||
message string |
||||
|
||||
// responses
|
||||
legacyResponse *NormalResponse |
||||
newResponse *NormalResponse |
||||
fallbackUseNew bool |
||||
compareErr bool |
||||
}{ |
||||
{ |
||||
name: "base case", |
||||
|
||||
legacyResponse: &NormalResponse{}, |
||||
newResponse: &NormalResponse{ |
||||
status: http.StatusInternalServerError, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "not found error", |
||||
|
||||
err: errors.New("not found"), |
||||
statusCode: http.StatusNotFound, |
||||
message: fakeNotFoundMessage, |
||||
|
||||
legacyResponse: &NormalResponse{ |
||||
status: http.StatusNotFound, |
||||
errMessage: fakeNotFoundMessage, |
||||
}, |
||||
newResponse: &NormalResponse{ |
||||
status: http.StatusInternalServerError, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "grafana error with fallback to other error", |
||||
|
||||
err: errutil.NewBase(errutil.StatusTimeout, "thing.timeout").Errorf("whoops"), |
||||
statusCode: http.StatusBadRequest, |
||||
message: genericErrorMessage, |
||||
|
||||
legacyResponse: &NormalResponse{ |
||||
status: http.StatusBadRequest, |
||||
errMessage: genericErrorMessage, |
||||
}, |
||||
newResponse: &NormalResponse{ |
||||
status: http.StatusGatewayTimeout, |
||||
errMessage: errutil.StatusTimeout.String(), |
||||
}, |
||||
fallbackUseNew: true, |
||||
}, |
||||
} |
||||
|
||||
compareResponses := func(expected *NormalResponse, actual *NormalResponse, compareErr bool) func(t *testing.T) { |
||||
return func(t *testing.T) { |
||||
if expected == nil { |
||||
require.Nil(t, actual) |
||||
return |
||||
} |
||||
|
||||
require.NotNil(t, actual) |
||||
assert.Equal(t, expected.status, actual.status) |
||||
if expected.body != nil { |
||||
assert.Equal(t, expected.body.Bytes(), actual.body.Bytes()) |
||||
} |
||||
if expected.header != nil { |
||||
assert.EqualValues(t, expected.header, actual.header) |
||||
} |
||||
assert.Equal(t, expected.errMessage, actual.errMessage) |
||||
if compareErr { |
||||
assert.ErrorIs(t, expected.err, actual.err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
for _, tc := range cases { |
||||
tc := tc |
||||
t.Run( |
||||
tc.name+" Error", |
||||
compareResponses(tc.legacyResponse, Error( |
||||
tc.statusCode, |
||||
tc.message, |
||||
tc.err, |
||||
), tc.compareErr), |
||||
) |
||||
|
||||
t.Run( |
||||
tc.name+" Err", |
||||
compareResponses(tc.newResponse, Err( |
||||
tc.err, |
||||
), tc.compareErr), |
||||
) |
||||
|
||||
fallbackResponse := tc.legacyResponse |
||||
if tc.fallbackUseNew { |
||||
fallbackResponse = tc.newResponse |
||||
} |
||||
t.Run( |
||||
tc.name+" ErrOrFallback", |
||||
compareResponses(fallbackResponse, ErrOrFallback( |
||||
tc.statusCode, |
||||
tc.message, |
||||
tc.err, |
||||
), tc.compareErr), |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,100 @@ |
||||
package service |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore" |
||||
"github.com/grafana/grafana/pkg/services/datasources" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
) |
||||
|
||||
const ( |
||||
// Not set means migration has not happened
|
||||
secretMigrationStatusKey = "secretMigrationStatus" |
||||
// Migration happened with disableSecretCompatibility set to false
|
||||
compatibleSecretMigrationValue = "compatible" |
||||
// Migration happened with disableSecretCompatibility set to true
|
||||
completeSecretMigrationValue = "complete" |
||||
) |
||||
|
||||
type DataSourceSecretMigrationService struct { |
||||
dataSourcesService datasources.DataSourceService |
||||
kvStore *kvstore.NamespacedKVStore |
||||
features featuremgmt.FeatureToggles |
||||
} |
||||
|
||||
func ProvideDataSourceMigrationService( |
||||
dataSourcesService datasources.DataSourceService, |
||||
kvStore kvstore.KVStore, |
||||
features featuremgmt.FeatureToggles, |
||||
) *DataSourceSecretMigrationService { |
||||
return &DataSourceSecretMigrationService{ |
||||
dataSourcesService: dataSourcesService, |
||||
kvStore: kvstore.WithNamespace(kvStore, 0, secretType), |
||||
features: features, |
||||
} |
||||
} |
||||
|
||||
func (s *DataSourceSecretMigrationService) Migrate(ctx context.Context) error { |
||||
migrationStatus, _, err := s.kvStore.Get(ctx, secretMigrationStatusKey) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// If this flag is true, delete secrets from the legacy secrets store as they are migrated
|
||||
disableSecretsCompatibility := s.features.IsEnabled(featuremgmt.FlagDisableSecretsCompatibility) |
||||
// If migration hasn't happened, migrate to unified secrets and keep copy in legacy
|
||||
// If a complete migration happened and now backwards compatibility is enabled, copy secrets back to legacy
|
||||
needCompatibility := migrationStatus != compatibleSecretMigrationValue && !disableSecretsCompatibility |
||||
// If migration hasn't happened, migrate to unified secrets and delete from legacy
|
||||
// If a compatible migration happened and now compatibility is disabled, delete secrets from legacy
|
||||
needMigration := migrationStatus != completeSecretMigrationValue && disableSecretsCompatibility |
||||
|
||||
if needCompatibility || needMigration { |
||||
query := &datasources.GetAllDataSourcesQuery{} |
||||
err := s.dataSourcesService.GetAllDataSources(ctx, query) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, ds := range query.Result { |
||||
secureJsonData, err := s.dataSourcesService.DecryptedValues(ctx, ds) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// Secrets are set by the update data source function if the SecureJsonData is set in the command
|
||||
// Secrets are deleted by the update data source function if the disableSecretsCompatibility flag is enabled
|
||||
err = s.dataSourcesService.UpdateDataSource(ctx, &datasources.UpdateDataSourceCommand{ |
||||
Id: ds.Id, |
||||
OrgId: ds.OrgId, |
||||
Uid: ds.Uid, |
||||
Name: ds.Name, |
||||
JsonData: ds.JsonData, |
||||
SecureJsonData: secureJsonData, |
||||
|
||||
// These are needed by the SQL function due to UseBool and MustCols
|
||||
IsDefault: ds.IsDefault, |
||||
BasicAuth: ds.BasicAuth, |
||||
WithCredentials: ds.WithCredentials, |
||||
ReadOnly: ds.ReadOnly, |
||||
User: ds.User, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
if disableSecretsCompatibility { |
||||
err = s.kvStore.Set(ctx, secretMigrationStatusKey, completeSecretMigrationValue) |
||||
} else { |
||||
err = s.kvStore.Set(ctx, secretMigrationStatusKey, compatibleSecretMigrationValue) |
||||
} |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,340 @@ |
||||
package service |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/kvstore" |
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" |
||||
"github.com/grafana/grafana/pkg/services/datasources" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes" |
||||
secretsStore "github.com/grafana/grafana/pkg/services/secrets/kvstore" |
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
"github.com/stretchr/testify/assert" |
||||
) |
||||
|
||||
func SetupTestMigrationService(t *testing.T, sqlStore *sqlstore.SQLStore, kvStore kvstore.KVStore, secretsStore secretsStore.SecretsKVStore, compatibility bool) *DataSourceSecretMigrationService { |
||||
t.Helper() |
||||
cfg := &setting.Cfg{} |
||||
features := featuremgmt.WithFeatures() |
||||
if !compatibility { |
||||
features = featuremgmt.WithFeatures(featuremgmt.FlagDisableSecretsCompatibility, true) |
||||
} |
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) |
||||
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New().WithDisabled(), acmock.NewMockedPermissionsService()) |
||||
migService := ProvideDataSourceMigrationService(dsService, kvStore, features) |
||||
return migService |
||||
} |
||||
|
||||
func TestMigrate(t *testing.T) { |
||||
t.Run("should migrate from legacy to unified without compatibility", func(t *testing.T) { |
||||
sqlStore := sqlstore.InitTestDB(t) |
||||
kvStore := kvstore.ProvideService(sqlStore) |
||||
secretsStore := secretsStore.SetupTestService(t) |
||||
migService := SetupTestMigrationService(t, sqlStore, kvStore, secretsStore, false) |
||||
|
||||
dataSourceName := "Test" |
||||
dataSourceOrg := int64(1) |
||||
|
||||
// Add test data source
|
||||
err := sqlStore.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ |
||||
OrgId: dataSourceOrg, |
||||
Name: dataSourceName, |
||||
Type: datasources.DS_MYSQL, |
||||
Access: datasources.DS_ACCESS_DIRECT, |
||||
Url: "http://test", |
||||
EncryptedSecureJsonData: map[string][]byte{ |
||||
"password": []byte("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"), |
||||
}, |
||||
}) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secret json data was added
|
||||
query := &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.NotEmpty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the migration status key is empty
|
||||
value, exist, err := kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Empty(t, value) |
||||
assert.False(t, exist) |
||||
|
||||
// Check that the secret is not present on the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.Empty(t, value) |
||||
assert.False(t, exist) |
||||
|
||||
// Run the migration
|
||||
err = migService.Migrate(context.Background()) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secure json data was deleted
|
||||
query = &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.Empty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the secret was added to the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.NotEmpty(t, value) |
||||
assert.True(t, exist) |
||||
|
||||
// Check if the migration status key was set
|
||||
value, exist, err = kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, completeSecretMigrationValue, value) |
||||
assert.True(t, exist) |
||||
}) |
||||
|
||||
t.Run("should migrate from legacy to unified with compatibility", func(t *testing.T) { |
||||
sqlStore := sqlstore.InitTestDB(t) |
||||
kvStore := kvstore.ProvideService(sqlStore) |
||||
secretsStore := secretsStore.SetupTestService(t) |
||||
migService := SetupTestMigrationService(t, sqlStore, kvStore, secretsStore, true) |
||||
|
||||
dataSourceName := "Test" |
||||
dataSourceOrg := int64(1) |
||||
|
||||
// Add test data source
|
||||
err := sqlStore.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ |
||||
OrgId: dataSourceOrg, |
||||
Name: dataSourceName, |
||||
Type: datasources.DS_MYSQL, |
||||
Access: datasources.DS_ACCESS_DIRECT, |
||||
Url: "http://test", |
||||
EncryptedSecureJsonData: map[string][]byte{ |
||||
"password": []byte("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"), |
||||
}, |
||||
}) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secret json data was added
|
||||
query := &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.NotEmpty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the migration status key is empty
|
||||
value, exist, err := kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Empty(t, value) |
||||
assert.False(t, exist) |
||||
|
||||
// Check that the secret is not present on the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.Empty(t, value) |
||||
assert.False(t, exist) |
||||
|
||||
// Run the migration
|
||||
err = migService.Migrate(context.Background()) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secure json data was maintained for compatibility
|
||||
query = &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.NotEmpty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the secret was added to the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.NotEmpty(t, value) |
||||
assert.True(t, exist) |
||||
|
||||
// Check if the migration status key was set
|
||||
value, exist, err = kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, compatibleSecretMigrationValue, value) |
||||
assert.True(t, exist) |
||||
}) |
||||
|
||||
t.Run("should replicate from unified to legacy for compatibility", func(t *testing.T) { |
||||
sqlStore := sqlstore.InitTestDB(t) |
||||
kvStore := kvstore.ProvideService(sqlStore) |
||||
secretsStore := secretsStore.SetupTestService(t) |
||||
migService := SetupTestMigrationService(t, sqlStore, kvStore, secretsStore, false) |
||||
|
||||
dataSourceName := "Test" |
||||
dataSourceOrg := int64(1) |
||||
|
||||
// Add test data source
|
||||
err := sqlStore.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ |
||||
OrgId: dataSourceOrg, |
||||
Name: dataSourceName, |
||||
Type: datasources.DS_MYSQL, |
||||
Access: datasources.DS_ACCESS_DIRECT, |
||||
Url: "http://test", |
||||
EncryptedSecureJsonData: map[string][]byte{ |
||||
"password": []byte("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"), |
||||
}, |
||||
}) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secret json data was added
|
||||
query := &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.NotEmpty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the migration status key is empty
|
||||
value, exist, err := kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Empty(t, value) |
||||
assert.False(t, exist) |
||||
|
||||
// Check that the secret is not present on the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.Empty(t, value) |
||||
assert.False(t, exist) |
||||
|
||||
// Run the migration without compatibility
|
||||
err = migService.Migrate(context.Background()) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secure json data was deleted
|
||||
query = &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.Empty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the secret was added to the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.NotEmpty(t, value) |
||||
assert.True(t, exist) |
||||
|
||||
// Check if the migration status key was set
|
||||
value, exist, err = kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, completeSecretMigrationValue, value) |
||||
assert.True(t, exist) |
||||
|
||||
// Run the migration with compatibility
|
||||
migService = SetupTestMigrationService(t, sqlStore, kvStore, secretsStore, true) |
||||
err = migService.Migrate(context.Background()) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secure json data was re-added for compatibility
|
||||
query = &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.NotEmpty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the secret was added to the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.NotEmpty(t, value) |
||||
assert.True(t, exist) |
||||
|
||||
// Check if the migration status key was set
|
||||
value, exist, err = kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, compatibleSecretMigrationValue, value) |
||||
assert.True(t, exist) |
||||
}) |
||||
|
||||
t.Run("should delete from legacy to remove compatibility", func(t *testing.T) { |
||||
sqlStore := sqlstore.InitTestDB(t) |
||||
kvStore := kvstore.ProvideService(sqlStore) |
||||
secretsStore := secretsStore.SetupTestService(t) |
||||
migService := SetupTestMigrationService(t, sqlStore, kvStore, secretsStore, true) |
||||
|
||||
dataSourceName := "Test" |
||||
dataSourceOrg := int64(1) |
||||
|
||||
// Add test data source
|
||||
err := sqlStore.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ |
||||
OrgId: dataSourceOrg, |
||||
Name: dataSourceName, |
||||
Type: datasources.DS_MYSQL, |
||||
Access: datasources.DS_ACCESS_DIRECT, |
||||
Url: "http://test", |
||||
EncryptedSecureJsonData: map[string][]byte{ |
||||
"password": []byte("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"), |
||||
}, |
||||
}) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secret json data was added
|
||||
query := &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.NotEmpty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the migration status key is empty
|
||||
value, exist, err := kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Empty(t, value) |
||||
assert.False(t, exist) |
||||
|
||||
// Check that the secret is not present on the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.Empty(t, value) |
||||
assert.False(t, exist) |
||||
|
||||
// Run the migration with compatibility
|
||||
err = migService.Migrate(context.Background()) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secure json data was maintained for compatibility
|
||||
query = &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.NotEmpty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the secret was added to the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.NotEmpty(t, value) |
||||
assert.True(t, exist) |
||||
|
||||
// Check if the migration status key was set
|
||||
value, exist, err = kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, compatibleSecretMigrationValue, value) |
||||
assert.True(t, exist) |
||||
|
||||
// Run the migration without compatibility
|
||||
migService = SetupTestMigrationService(t, sqlStore, kvStore, secretsStore, false) |
||||
err = migService.Migrate(context.Background()) |
||||
assert.NoError(t, err) |
||||
|
||||
// Check if the secure json data was deleted
|
||||
query = &datasources.GetDataSourceQuery{OrgId: dataSourceOrg, Name: dataSourceName} |
||||
err = sqlStore.GetDataSource(context.Background(), query) |
||||
assert.NoError(t, err) |
||||
assert.NotNil(t, query.Result) |
||||
assert.Empty(t, query.Result.SecureJsonData) |
||||
|
||||
// Check if the secret was added to the secret store
|
||||
value, exist, err = secretsStore.Get(context.Background(), dataSourceOrg, dataSourceName, secretType) |
||||
assert.NoError(t, err) |
||||
assert.NotEmpty(t, value) |
||||
assert.True(t, exist) |
||||
|
||||
// Check if the migration status key was set
|
||||
value, exist, err = kvStore.Get(context.Background(), 0, secretType, secretMigrationStatusKey) |
||||
assert.NoError(t, err) |
||||
assert.Equal(t, completeSecretMigrationValue, value) |
||||
assert.True(t, exist) |
||||
}) |
||||
} |
@ -0,0 +1,51 @@ |
||||
package export |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"path" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
) |
||||
|
||||
func exportAlerts(helper *commitHelper, job *gitExportJob) error { |
||||
alertDir := path.Join(helper.orgDir, "alerts") |
||||
|
||||
return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { |
||||
type ruleResult struct { |
||||
Title string `xorm:"title"` |
||||
UID string `xorm:"uid"` |
||||
NamespaceUID string `xorm:"namespace_uid"` |
||||
RuleGroup string `xorm:"rule_group"` |
||||
Condition json.RawMessage `xorm:"data"` |
||||
DashboardUID string `xorm:"dashboard_uid"` |
||||
PanelID int64 `xorm:"panel_id"` |
||||
Updated time.Time `xorm:"updated" json:"-"` |
||||
} |
||||
|
||||
rows := make([]*ruleResult, 0) |
||||
|
||||
sess.Table("alert_rule").Where("org_id = ?", helper.orgID) |
||||
|
||||
err := sess.Find(&rows) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, row := range rows { |
||||
err = helper.add(commitOptions{ |
||||
body: []commitBody{{ |
||||
body: prettyJSON(row), |
||||
fpath: path.Join(alertDir, row.UID) + ".json", // must be JSON files
|
||||
}}, |
||||
comment: fmt.Sprintf("Alert: %s", row.Title), |
||||
when: row.Updated, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return err |
||||
}) |
||||
} |
@ -0,0 +1,80 @@ |
||||
package export |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"path/filepath" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
) |
||||
|
||||
func exportDashboardThumbnails(helper *commitHelper, job *gitExportJob) error { |
||||
alias := make(map[string]string, 100) |
||||
aliasLookup, err := ioutil.ReadFile(filepath.Join(helper.orgDir, "root-alias.json")) |
||||
if err != nil { |
||||
return fmt.Errorf("missing dashboard alias files (must export dashboards first)") |
||||
} |
||||
err = json.Unmarshal(aliasLookup, &alias) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { |
||||
type dashboardThumb struct { |
||||
UID string `xorm:"uid"` |
||||
Image []byte `xorm:"image"` |
||||
Theme string `xorm:"theme"` |
||||
Kind string `xorm:"kind"` |
||||
MimeType string `xorm:"mime_type"` |
||||
Updated time.Time |
||||
} |
||||
|
||||
rows := make([]*dashboardThumb, 0) |
||||
|
||||
// SELECT uid,image,theme,kind,mime_type,dashboard_thumbnail.updated
|
||||
// FROM dashboard_thumbnail
|
||||
// JOIN dashboard ON dashboard.id = dashboard_thumbnail.dashboard_id
|
||||
// WHERE org_id = 2; //dashboard.uid = '2VVbg06nz';
|
||||
|
||||
sess.Table("dashboard_thumbnail"). |
||||
Join("INNER", "dashboard", "dashboard.id = dashboard_thumbnail.dashboard_id"). |
||||
Cols("uid", "image", "theme", "kind", "mime_type", "dashboard_thumbnail.updated"). |
||||
Where("dashboard.org_id = ?", helper.orgID) |
||||
|
||||
err := sess.Find(&rows) |
||||
if err != nil { |
||||
if strings.HasPrefix(err.Error(), "no such table") { |
||||
return nil |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// Process all folders
|
||||
for _, row := range rows { |
||||
p, ok := alias[row.UID] |
||||
if !ok { |
||||
p = "uid/" + row.UID |
||||
} else { |
||||
p = strings.TrimSuffix(p, "-dash.json") |
||||
} |
||||
|
||||
err := helper.add(commitOptions{ |
||||
body: []commitBody{ |
||||
{ |
||||
fpath: filepath.Join(helper.orgDir, "thumbs", fmt.Sprintf("%s.thumb-%s.png", p, row.Theme)), |
||||
body: row.Image, |
||||
}, |
||||
}, |
||||
when: row.Updated, |
||||
comment: "Thumbnail", |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
}) |
||||
} |
@ -0,0 +1,53 @@ |
||||
package export |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"path" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
) |
||||
|
||||
func exportPlugins(helper *commitHelper, job *gitExportJob) error { |
||||
return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { |
||||
type pResult struct { |
||||
PluginID string `xorm:"plugin_id" json:"-"` |
||||
Enabled string `xorm:"enabled" json:"enabled"` |
||||
Pinned string `xorm:"pinned" json:"pinned"` |
||||
JSONData json.RawMessage `xorm:"json_data" json:"json_data,omitempty"` |
||||
// TODO: secure!!!!
|
||||
PluginVersion string `xorm:"plugin_version" json:"version"` |
||||
Created time.Time `xorm:"created" json:"created"` |
||||
Updated time.Time `xorm:"updated" json:"updated"` |
||||
} |
||||
|
||||
rows := make([]*pResult, 0) |
||||
|
||||
sess.Table("plugin_setting").Where("org_id = ?", helper.orgID) |
||||
|
||||
err := sess.Find(&rows) |
||||
if err != nil { |
||||
if strings.HasPrefix(err.Error(), "no such table") { |
||||
return nil |
||||
} |
||||
return err |
||||
} |
||||
|
||||
for _, row := range rows { |
||||
err = helper.add(commitOptions{ |
||||
body: []commitBody{{ |
||||
body: prettyJSON(row), |
||||
fpath: path.Join(helper.orgDir, "plugins", row.PluginID, "settings.json"), |
||||
}}, |
||||
comment: fmt.Sprintf("Plugin: %s", row.PluginID), |
||||
when: row.Updated, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return err |
||||
}) |
||||
} |
@ -0,0 +1,72 @@ |
||||
package export |
||||
|
||||
import ( |
||||
"fmt" |
||||
"path/filepath" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
) |
||||
|
||||
func exportSystemShortURL(helper *commitHelper, job *gitExportJob) error { |
||||
mostRecent := int64(0) |
||||
lastSeen := make(map[string]int64, 50) |
||||
dir := filepath.Join(helper.orgDir, "system", "short_url") |
||||
|
||||
err := job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { |
||||
type urlResult struct { |
||||
UID string `xorm:"uid" json:"-"` |
||||
Path string `xorm:"path" json:"path"` |
||||
CreatedBy int64 `xorm:"created_by" json:"-"` |
||||
CreatedAt time.Time `xorm:"created_at" json:"-"` |
||||
LastSeenAt int64 `xorm:"last_seen_at" json:"-"` |
||||
} |
||||
|
||||
rows := make([]*urlResult, 0) |
||||
|
||||
sess.Table("short_url").Where("org_id = ?", helper.orgID) |
||||
|
||||
err := sess.Find(&rows) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for _, row := range rows { |
||||
if row.LastSeenAt > 0 { |
||||
lastSeen[row.UID] = row.LastSeenAt |
||||
if mostRecent < row.LastSeenAt { |
||||
mostRecent = row.LastSeenAt |
||||
} |
||||
} |
||||
err := helper.add(commitOptions{ |
||||
body: []commitBody{ |
||||
{ |
||||
fpath: filepath.Join(dir, "uid", fmt.Sprintf("%s.json", row.UID)), |
||||
body: prettyJSON(row), |
||||
}, |
||||
}, |
||||
when: row.CreatedAt, |
||||
comment: "short URL", |
||||
userID: row.CreatedBy, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return err |
||||
}) |
||||
if err != nil || len(lastSeen) < 1 { |
||||
return err |
||||
} |
||||
|
||||
return helper.add(commitOptions{ |
||||
body: []commitBody{ |
||||
{ |
||||
fpath: filepath.Join(dir, "last_seen_at.json"), |
||||
body: prettyJSON(lastSeen), |
||||
}, |
||||
}, |
||||
when: time.UnixMilli(mostRecent), |
||||
comment: "short URL", |
||||
}) |
||||
} |
@ -0,0 +1,85 @@ |
||||
package export |
||||
|
||||
import ( |
||||
"path" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
) |
||||
|
||||
func exportUsage(helper *commitHelper, job *gitExportJob) error { |
||||
return job.sql.WithDbSession(helper.ctx, func(sess *sqlstore.DBSession) error { |
||||
commit := commitOptions{ |
||||
comment: "usage stats", |
||||
} |
||||
|
||||
type statsTables struct { |
||||
table string |
||||
sql string |
||||
converters []sqlutil.Converter |
||||
} |
||||
|
||||
dump := []statsTables{ |
||||
{ |
||||
table: "data_source_usage_by_day", |
||||
sql: `SELECT day,uid,queries,errors,load_duration_ms
|
||||
FROM data_source_usage_by_day
|
||||
JOIN data_source ON data_source.id = data_source_usage_by_day.data_source_id |
||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10), |
||||
converters: []sqlutil.Converter{{Dynamic: true}}, |
||||
}, |
||||
{ |
||||
table: "dashboard_usage_by_day", |
||||
sql: `SELECT uid,day,views,queries,errors,load_duration
|
||||
FROM dashboard_usage_by_day |
||||
JOIN dashboard ON dashboard_usage_by_day.dashboard_id = dashboard.id |
||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10), |
||||
converters: []sqlutil.Converter{{Dynamic: true}}, |
||||
}, |
||||
{ |
||||
table: "dashboard_usage_sums", |
||||
sql: `SELECT uid, |
||||
views_last_1_days, |
||||
views_last_7_days, |
||||
views_last_30_days, |
||||
views_total, |
||||
queries_last_1_days, |
||||
queries_last_7_days, |
||||
queries_last_30_days, |
||||
queries_total, |
||||
errors_last_1_days, |
||||
errors_last_7_days, |
||||
errors_last_30_days, |
||||
errors_total |
||||
FROM dashboard_usage_sums |
||||
JOIN dashboard ON dashboard_usage_sums.dashboard_id = dashboard.id |
||||
WHERE org_id =` + strconv.FormatInt(helper.orgID, 10), |
||||
converters: []sqlutil.Converter{{Dynamic: true}}, |
||||
}, |
||||
} |
||||
|
||||
for _, usage := range dump { |
||||
rows, err := sess.DB().QueryContext(helper.ctx, usage.sql) |
||||
if err != nil { |
||||
if strings.HasPrefix(err.Error(), "no such table") { |
||||
continue |
||||
} |
||||
return err |
||||
} |
||||
|
||||
frame, err := sqlutil.FrameFromRows(rows.Rows, -1, usage.converters...) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
frame.Name = usage.table |
||||
commit.body = append(commit.body, commitBody{ |
||||
fpath: path.Join(helper.orgDir, "usage", usage.table+".json"), |
||||
frame: frame, |
||||
}) |
||||
} |
||||
|
||||
return helper.add(commit) |
||||
}) |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue