Merge branch 'main' into drclau/unistor/replace-authenticators-3
This commit is contained in:
+3
-3
@@ -36,7 +36,7 @@
|
|||||||
/docs/ @grafana/docs-tooling
|
/docs/ @grafana/docs-tooling
|
||||||
|
|
||||||
/docs/.codespellignore @grafana/docs-tooling
|
/docs/.codespellignore @grafana/docs-tooling
|
||||||
/docs/sources/ @Eve832
|
/docs/sources/ @irenerl24
|
||||||
|
|
||||||
/docs/sources/administration/ @jdbaldry
|
/docs/sources/administration/ @jdbaldry
|
||||||
/docs/sources/alerting/ @brendamuir
|
/docs/sources/alerting/ @brendamuir
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
/pkg/services/annotations/ @grafana/grafana-search-and-storage
|
/pkg/services/annotations/ @grafana/grafana-search-and-storage
|
||||||
/pkg/services/apikey/ @grafana/identity-squad
|
/pkg/services/apikey/ @grafana/identity-squad
|
||||||
/pkg/services/cleanup/ @grafana/grafana-backend-group
|
/pkg/services/cleanup/ @grafana/grafana-backend-group
|
||||||
/pkg/services/contexthandler/ @grafana/grafana-backend-group
|
/pkg/services/contexthandler/ @grafana/grafana-backend-group @grafana/grafana-app-platform-squad
|
||||||
/pkg/services/correlations/ @grafana/explore-squad
|
/pkg/services/correlations/ @grafana/explore-squad
|
||||||
/pkg/services/dashboardimport/ @grafana/grafana-backend-group
|
/pkg/services/dashboardimport/ @grafana/grafana-backend-group
|
||||||
/pkg/services/dashboards/ @grafana/grafana-app-platform-squad
|
/pkg/services/dashboards/ @grafana/grafana-app-platform-squad
|
||||||
@@ -320,7 +320,7 @@
|
|||||||
/e2e/ @grafana/grafana-frontend-platform
|
/e2e/ @grafana/grafana-frontend-platform
|
||||||
/e2e/cloud-plugins-suite/ @grafana/partner-datasources
|
/e2e/cloud-plugins-suite/ @grafana/partner-datasources
|
||||||
/e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
|
/e2e/plugin-e2e/plugin-e2e-api-tests/ @grafana/plugins-platform-frontend
|
||||||
/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend
|
/e2e/test-plugins/grafana-extensionstest-app/ @grafana/plugins-platform-frontend
|
||||||
|
|
||||||
# Packages
|
# Packages
|
||||||
/packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
|
/packages/ @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ In [Grafana Enterprise]({{< relref "../../introduction/grafana-enterprise/" >}})
|
|||||||
|
|
||||||
{{% admonition type="note" %}}
|
{{% admonition type="note" %}}
|
||||||
Service accounts can only act in the organization they are created for. If you have the same task that is needed for multiple organizations, we recommend creating service accounts in each organization.
|
Service accounts can only act in the organization they are created for. If you have the same task that is needed for multiple organizations, we recommend creating service accounts in each organization.
|
||||||
|
|
||||||
|
Service accounts can't be used for instance-wide operations, such as global user management and organization management. For these tasks, you need to use a user with [Grafana server administrator permissions]({{< relref "../roles-and-permissions/#grafana-server-administrators" >}}).
|
||||||
{{% /admonition %}}
|
{{% /admonition %}}
|
||||||
|
|
||||||
{{< vimeo 742056367 >}}
|
{{< vimeo 742056367 >}}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ In the following JSON, id is shown as null which is the default value assigned t
|
|||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {
|
"timepicker": {
|
||||||
"time_options": [],
|
|
||||||
"refresh_intervals": []
|
"refresh_intervals": []
|
||||||
},
|
},
|
||||||
"templating": {
|
"templating": {
|
||||||
@@ -137,17 +136,6 @@ The grid has a negative gravity that moves panels up if there is empty space abo
|
|||||||
"now": true,
|
"now": true,
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"nowDelay": "",
|
"nowDelay": "",
|
||||||
"time_options": [
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"1h",
|
|
||||||
"6h",
|
|
||||||
"12h",
|
|
||||||
"24h",
|
|
||||||
"2d",
|
|
||||||
"7d",
|
|
||||||
"30d"
|
|
||||||
],
|
|
||||||
"refresh_intervals": [
|
"refresh_intervals": [
|
||||||
"5s",
|
"5s",
|
||||||
"10s",
|
"10s",
|
||||||
@@ -175,7 +163,6 @@ Usage of the fields is explained below:
|
|||||||
| **now** | |
|
| **now** | |
|
||||||
| **hidden** | whether timepicker is hidden or not |
|
| **hidden** | whether timepicker is hidden or not |
|
||||||
| **nowDelay** | override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. |
|
| **nowDelay** | override the now time by entering a time delay. Use this option to accommodate known delays in data aggregation to avoid null values. |
|
||||||
| **time_options** | options available in the time picker dropdown |
|
|
||||||
| **refresh_intervals** | interval options available in the refresh picker dropdown |
|
| **refresh_intervals** | interval options available in the refresh picker dropdown |
|
||||||
| **status** | |
|
| **status** | |
|
||||||
| **type** | |
|
| **type** | |
|
||||||
|
|||||||
@@ -18,9 +18,15 @@ title: 'Admin HTTP API '
|
|||||||
|
|
||||||
# Admin API
|
# Admin API
|
||||||
|
|
||||||
The Admin HTTP API does not currently work with an API Token. API Tokens are currently only linked to an organization and an organization role. They cannot be given
|
{{< admonition type="caution" >}}
|
||||||
the permission of server admin, only users can be given that permission. So in order to use these API calls you will have to use Basic Auth and the Grafana user
|
You can't authenticate to the Admin HTTP API with service account tokens.
|
||||||
must have the Grafana Admin permission. (The default admin user is called `admin` and has permission to use this API.)
|
Service accounts are limited to an organization and an organization role.
|
||||||
|
They can't be granted [Grafana server administrator permissions](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators).
|
||||||
|
|
||||||
|
To use these API endpoints you have to use Basic authentication and the Grafana user must have the Grafana server administrator permission.
|
||||||
|
|
||||||
|
The `admin` user that Grafana is provisioned with by default has permissions to use these API endpoints.
|
||||||
|
{{< /admonition >}}
|
||||||
|
|
||||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information.
|
||||||
|
|
||||||
|
|||||||
@@ -217,31 +217,7 @@ Content-Length: 1300
|
|||||||
"from": "now-6h",
|
"from": "now-6h",
|
||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {
|
"timepicker": {},
|
||||||
"refresh_intervals": [
|
|
||||||
"5s",
|
|
||||||
"10s",
|
|
||||||
"30s",
|
|
||||||
"1m",
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"30m",
|
|
||||||
"1h",
|
|
||||||
"2h",
|
|
||||||
"1d"
|
|
||||||
],
|
|
||||||
"time_options": [
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"1h",
|
|
||||||
"6h",
|
|
||||||
"12h",
|
|
||||||
"24h",
|
|
||||||
"2d",
|
|
||||||
"7d",
|
|
||||||
"30d"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "test",
|
"title": "test",
|
||||||
"version": 1
|
"version": 1
|
||||||
@@ -328,31 +304,7 @@ Content-Length: 1300
|
|||||||
"from": "now-6h",
|
"from": "now-6h",
|
||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {
|
"timepicker": {},
|
||||||
"refresh_intervals": [
|
|
||||||
"5s",
|
|
||||||
"10s",
|
|
||||||
"30s",
|
|
||||||
"1m",
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"30m",
|
|
||||||
"1h",
|
|
||||||
"2h",
|
|
||||||
"1d"
|
|
||||||
],
|
|
||||||
"time_options": [
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"1h",
|
|
||||||
"6h",
|
|
||||||
"12h",
|
|
||||||
"24h",
|
|
||||||
"2d",
|
|
||||||
"7d",
|
|
||||||
"30d"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "test",
|
"title": "test",
|
||||||
"version": 1
|
"version": 1
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ title: Licensing HTTP API
|
|||||||
|
|
||||||
Licensing is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "/docs/grafana/latest/introduction/grafana-enterprise" >}}).
|
Licensing is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "/docs/grafana/latest/introduction/grafana-enterprise" >}}).
|
||||||
|
|
||||||
|
{{< admonition type="caution" >}}
|
||||||
|
You can't authenticate to the Licensing HTTP API with service account tokens.
|
||||||
|
Service accounts are limited to an organization and an organization role.
|
||||||
|
They can't be granted [Grafana server administrator permissions](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators).
|
||||||
|
|
||||||
|
To use these API endpoints you have to use Basic authentication and the Grafana user must have the Grafana server administrator permission.
|
||||||
|
|
||||||
|
The `admin` user that Grafana is provisioned with by default has permissions to use these API endpoints.
|
||||||
|
{{< /admonition >}}
|
||||||
|
|
||||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "/docs/grafana/latest/administration/roles-and-permissions/access-control/custom-role-actions-scopes" >}}) for more information.
|
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "/docs/grafana/latest/administration/roles-and-permissions/access-control/custom-role-actions-scopes" >}}) for more information.
|
||||||
|
|
||||||
## Check license availability
|
## Check license availability
|
||||||
|
|||||||
@@ -282,11 +282,15 @@ Content-Type: application/json
|
|||||||
|
|
||||||
## Admin Organizations API
|
## Admin Organizations API
|
||||||
|
|
||||||
The Admin Organizations HTTP API does not currently work with an API Token. API Tokens are currently
|
{{< admonition type="caution" >}}
|
||||||
only linked to an organization and an organization role. They cannot be given the permission of server
|
You can't authenticate to the Admin Organizations HTTP API with service account tokens.
|
||||||
admin, only users can be given that permission. So in order to use these API calls you will have to
|
Service accounts are limited to an organization and an organization role.
|
||||||
use Basic Auth and the Grafana user must have the Grafana Admin permission (The default admin user
|
They can't be granted [Grafana server administrator permissions](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators).
|
||||||
is called `admin` and has permission to use this API).
|
|
||||||
|
To use these API endpoints you have to use Basic authentication and the Grafana user must have the Grafana server administrator permission.
|
||||||
|
|
||||||
|
The `admin` user that Grafana is provisioned with by default has permissions to use these API endpoints.
|
||||||
|
{{< /admonition >}}
|
||||||
|
|
||||||
### Get Organization by Id
|
### Get Organization by Id
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,17 @@ refs:
|
|||||||
|
|
||||||
# User API
|
# User API
|
||||||
|
|
||||||
The Users HTTP API does not currently work with an API Token. API Tokens are linked to an organization and an organization role. They cannot be given
|
{{< admonition type="caution" >}}
|
||||||
the permission of server users access, only users can be given that permission. To use these API calls you can use Basic Auth and the Grafana
|
You can't authenticate to the User HTTP API with service account tokens.
|
||||||
user must have the Grafana Admin role.
|
Service accounts are limited to an organization and an organization role.
|
||||||
|
They can't be granted [Grafana server administrator permissions](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/#grafana-server-administrators).
|
||||||
|
|
||||||
API Tokens can be used with Organization HTTP API to get users of specific organization.
|
Alternatively, you can use the [Organization HTTP API](/docs/grafana/<GRAFANA_VERSION>/developers/http_api/org/#current-organization-api) with service account tokens to manage users in a specific organization
|
||||||
|
|
||||||
|
To use these API endpoints you have to use Basic authentication and the Grafana user must have the Grafana server administrator permission.
|
||||||
|
|
||||||
|
The `admin` user that Grafana is provisioned with by default has permissions to use these API endpoints.
|
||||||
|
{{< /admonition >}}
|
||||||
|
|
||||||
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions](ref:role-based-access-control-permissions) for more information.
|
> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions](ref:role-based-access-control-permissions) for more information.
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ labels:
|
|||||||
- enterprise
|
- enterprise
|
||||||
- oss
|
- oss
|
||||||
title: Correlations Editor in Explore
|
title: Correlations Editor in Explore
|
||||||
weight: 400
|
weight: 20
|
||||||
---
|
---
|
||||||
|
|
||||||
# Correlations Editor in Explore
|
# Correlations Editor in Explore
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ labels:
|
|||||||
keywords:
|
keywords:
|
||||||
- Explore
|
- Explore
|
||||||
title: Query inspector in Explore
|
title: Query inspector in Explore
|
||||||
weight: 40
|
weight: 15
|
||||||
---
|
---
|
||||||
|
|
||||||
# Query inspector in Explore
|
# Query inspector in Explore
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ labels:
|
|||||||
- enterprise
|
- enterprise
|
||||||
- oss
|
- oss
|
||||||
title: Logs in Explore
|
title: Logs in Explore
|
||||||
weight: 15
|
weight: 25
|
||||||
---
|
---
|
||||||
|
|
||||||
# Logs in Explore
|
# Logs in Explore
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ labels:
|
|||||||
- enterprise
|
- enterprise
|
||||||
- oss
|
- oss
|
||||||
title: Traces in Explore
|
title: Traces in Explore
|
||||||
weight: 20
|
weight: 40
|
||||||
---
|
---
|
||||||
|
|
||||||
# Traces in Explore
|
# Traces in Explore
|
||||||
|
|||||||
@@ -41,31 +41,57 @@ refs:
|
|||||||
|
|
||||||
# Bar chart
|
# Bar chart
|
||||||
|
|
||||||
Bar charts allow you to graph categorical data.
|
A bar chart is a visual representation that uses rectangular bars, where the length of each bar represents each value.
|
||||||
|
You can use the bar chart visualization when you want to compare values over different categories or time periods. The visualization can display the bars horizontally or vertically, and can be customized to group or stack bars for more complex data analysis.
|
||||||
|
|
||||||
{{< figure src="/static/img/docs/bar-chart-panel/barchart_small_example.png" max-width="1000px" caption="Bar chart" >}}
|
{{< figure src="/static/img/docs/bar-chart-panel/barchart_small_example.png" max-width="1000px" alt="Bar chart" >}}
|
||||||
|
|
||||||
|
You can use the bar chart visualization if you need to show:
|
||||||
|
|
||||||
|
- Population distribution by age or location
|
||||||
|
- CPU usage per application
|
||||||
|
- Sales per division
|
||||||
|
- Server cost distribution
|
||||||
|
|
||||||
|
## Configure a bar chart
|
||||||
|
|
||||||
|
The following video shows you how to create and configure a bar chart visualization:
|
||||||
|
|
||||||
|
{{< youtube id="qyKE9-71KkE" >}}
|
||||||
|
|
||||||
{{< docs/play title="Grafana Bar Charts and Pie Charts" url="https://play.grafana.org/d/ktMs4D6Mk/" >}}
|
{{< docs/play title="Grafana Bar Charts and Pie Charts" url="https://play.grafana.org/d/ktMs4D6Mk/" >}}
|
||||||
|
|
||||||
## Supported data formats
|
## Supported data formats
|
||||||
|
|
||||||
Only one data frame is supported and it must have at least one string field that will be used as the category for an X or Y axis and one or more numerical fields.
|
To create a bar chart visualization, you need a dataset containing one string or time field (or column) and at least one numeric field, though preferably more than one to make best use of the visualization.
|
||||||
|
|
||||||
Example:
|
The text or time field is used to label the bars or values in each row of data and the numeric fields are represented by proportionally sized bars.
|
||||||
|
|
||||||
| Browser | Market share |
|
### Example 1
|
||||||
| ------- | ------------ |
|
|
||||||
| Chrome | 50 |
|
|
||||||
| IE | 17.5 |
|
|
||||||
|
|
||||||
If you have more than one numerical field the visualization will show grouped bars.
|
| Group | Value1 | Value2 | Value3 |
|
||||||
|
| ----- | ------ | ------ | ------ |
|
||||||
|
| uno | 5 | 3 | 2 |
|
||||||
|
|
||||||
### Visualizing time series or multiple result sets
|

|
||||||
|
|
||||||
If you have multiple time series or tables you first need to join them using a join or reduce transform. For example if you
|
If you have more than one text or time field, by default, the visualization uses the first one, but you can change this in the x-axis option as described in the [Bar chart options](#bar-chart-options) section.
|
||||||
have multiple time series and you want to compare their last and max value add the **Reduce** transform and specify **Max** and **Last** as options under **Calculations**.
|
|
||||||
|
|
||||||
{{< figure src="/static/img/docs/bar-chart-panel/bar-chart-time-series-v8-0.png" max-width="1025px" caption="Bar chart time series example" >}}
|
### Example 2
|
||||||
|
|
||||||
|
If your dataset contains multiple rows, the visualization displays multiple bar chart groups where each group contains multiple bars representing all the numeric values for a row.
|
||||||
|
|
||||||
|
| Group | Value1 | Value2 | Value3 |
|
||||||
|
| ----- | ------ | ------ | ------ |
|
||||||
|
| uno | 5 | 3 | 2 |
|
||||||
|
| dos | 10 | 6 | 4 |
|
||||||
|
| tres | 20 | 8 | 2 |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
While the first field can be time-based and you can use a bar chart to plot time-series data, for large amounts of time-series data, we recommend that you use the [time series visualization](https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/time-series/) and configure it to be displayed as bars.
|
||||||
|
|
||||||
|
We recommend that you only use one dataset in a bar chart because using multiple datasets can result in unexpected behavior.
|
||||||
|
|
||||||
## Panel options
|
## Panel options
|
||||||
|
|
||||||
@@ -75,6 +101,10 @@ have multiple time series and you want to compare their last and max value add t
|
|||||||
|
|
||||||
Use these options to refine your visualization.
|
Use these options to refine your visualization.
|
||||||
|
|
||||||
|
### X Axis
|
||||||
|
|
||||||
|
Specify which field is used for the x-axis.
|
||||||
|
|
||||||
### Orientation
|
### Orientation
|
||||||
|
|
||||||
- **Auto** - Grafana decides the bar orientation based on what the panel dimensions.
|
- **Auto** - Grafana decides the bar orientation based on what the panel dimensions.
|
||||||
|
|||||||
@@ -25,14 +25,70 @@ refs:
|
|||||||
|
|
||||||
# Bar gauge
|
# Bar gauge
|
||||||
|
|
||||||
Bar gauges simplify your data by reducing every field to a single value. You choose how Grafana calculates the reduction.
|
Bar gauges simplify your data by reducing every field to a single value. You choose how Grafana calculates the reduction. This visualization can show one or more bar gauges depending on how many series, rows, or columns your query returns.
|
||||||
|
|
||||||
This panel can show one or more bar gauges depending on how many series, rows, or columns your query returns.
|
|
||||||
|
|
||||||
{{< figure src="/static/img/docs/v66/bar_gauge_cover.png" max-width="1025px" alt="Bar gauge panel" >}}
|
{{< figure src="/static/img/docs/v66/bar_gauge_cover.png" max-width="1025px" alt="Bar gauge panel" >}}
|
||||||
|
|
||||||
|
The bar gauge visualization displays values as bars with various lengths or fills proportional to the values they represent. They differ from traditional bar charts in that they act as gauges displaying metrics between ranges. One example is a thermometer displaying body temperature in a bar filling up.
|
||||||
|
|
||||||
|
You can use a bar gauge visualization when you need to show:
|
||||||
|
|
||||||
|
- Key performance indicators (KPIs)
|
||||||
|
- System health
|
||||||
|
- Savings goals
|
||||||
|
- Attendance
|
||||||
|
- Process completion rates
|
||||||
|
|
||||||
{{< docs/play title="Bar Gauge" url="https://play.grafana.org/d/vmie2cmWz/" >}}
|
{{< docs/play title="Bar Gauge" url="https://play.grafana.org/d/vmie2cmWz/" >}}
|
||||||
|
|
||||||
|
## Supported data formats
|
||||||
|
|
||||||
|
To create a bar gauge visualization, you need a dataset querying at least one numeric field. Every numeric field in the dataset is displayed as a bar gauge. Text or time fields aren't required but if they're present, they're used for labeling.
|
||||||
|
|
||||||
|
### Example 1
|
||||||
|
|
||||||
|
| Label | Value1 | Value2 | Value3 |
|
||||||
|
| ----- | ------ | ------ | ------ |
|
||||||
|
| Row1 | 5 | 3 | 2 |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The minimum and maximum range for the bar gauges is automatically pulled from the largest and smallest numeric values in the dataset. You can also manually define the minimum and maximum values as indicated in the [Standard options](#standard-options) section.
|
||||||
|
|
||||||
|
You can also define the minimum and maximum from the dataset provided.
|
||||||
|
|
||||||
|
### Example 2
|
||||||
|
|
||||||
|
| Label | Value | Max | Min |
|
||||||
|
| ----- | ----- | --- | --- |
|
||||||
|
| Row1 | 3 | 6 | 1 |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If you don’t want to show gauges for the min and max values, you can configure only one field to be displayed as described in the [Value options](#value-options) section.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Even if the min and max aren’t displayed, the visualization still pulls the range from the data set.
|
||||||
|
|
||||||
|
### Example 3
|
||||||
|
|
||||||
|
The bar gauge visualization also supports multiple records (rows) in the dataset.
|
||||||
|
|
||||||
|
| Label | Value1 | Value2 | Value3 |
|
||||||
|
| ----- | ------ | ------ | ------ |
|
||||||
|
| Row1 | 5 | 3 | 2 |
|
||||||
|
| Row2 | 10 | 6 | 4 |
|
||||||
|
| Row3 | 20 | 8 | 2 |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
By default, the visualization is configured to [calculate](#value-options) a single value per column or series and to display only the last set of data. However, it derives the minimum and maximum from the full dataset even if those values aren’t visible. In this example, that means only the last row of data is displayed in the gauges and the minimum and maximum values are defined as 2 and 20, pulled from the whole dataset.
|
||||||
|
|
||||||
|
If you want to show one gauge per cell you can change the [Show](#show) setting from [Calculate](#calculate) to [All values](#all-values) and each bar is labeled by concatenating the text column with each value's column name.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Panel options
|
## Panel options
|
||||||
|
|
||||||
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
{{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||||
@@ -141,6 +197,10 @@ Automatically show y-axis scrollbar when there's a large amount of data.
|
|||||||
This option only applies when bar size is set to manual.
|
This option only applies when bar size is set to manual.
|
||||||
{{% /admonition %}}
|
{{% /admonition %}}
|
||||||
|
|
||||||
|
## Legend options
|
||||||
|
|
||||||
|
{{< docs/shared lookup="visualizations/legend-options-1.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||||
|
|
||||||
## Standard options
|
## Standard options
|
||||||
|
|
||||||
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
{{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||||
@@ -157,6 +217,10 @@ This option only applies when bar size is set to manual.
|
|||||||
|
|
||||||
{{< docs/shared lookup="visualizations/thresholds-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
{{< docs/shared lookup="visualizations/thresholds-options-2.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||||
|
|
||||||
|
Last, colors of the bar gauge thresholds can be configured as described above.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Field overrides
|
## Field overrides
|
||||||
|
|
||||||
{{< docs/shared lookup="visualizations/overrides-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
{{< docs/shared lookup="visualizations/overrides-options.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||||
|
|||||||
@@ -194,6 +194,10 @@ Experimental features might be changed or removed without prior notice.
|
|||||||
| `dataplaneAggregator` | Enable grafana dataplane aggregator |
|
| `dataplaneAggregator` | Enable grafana dataplane aggregator |
|
||||||
| `adhocFilterOneOf` | Exposes a new 'one of' operator for ad-hoc filters. This operator allows users to filter by multiple values in a single filter. |
|
| `adhocFilterOneOf` | Exposes a new 'one of' operator for ad-hoc filters. This operator allows users to filter by multiple values in a single filter. |
|
||||||
| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying |
|
| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying |
|
||||||
|
| `singleTopNav` | Unifies the top search bar and breadcrumb bar into one |
|
||||||
|
| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards |
|
||||||
|
| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics |
|
||||||
|
| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range |
|
||||||
|
|
||||||
## Development feature toggles
|
## Development feature toggles
|
||||||
|
|
||||||
|
|||||||
@@ -232,10 +232,7 @@ For more information on how to configure dashboard providers, refer to [Dashboar
|
|||||||
"from": "now-6h",
|
"from": "now-6h",
|
||||||
"to": "now"
|
"to": "now"
|
||||||
},
|
},
|
||||||
"timepicker": {
|
"timepicker": {},
|
||||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
|
||||||
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
|
|
||||||
},
|
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "Cluster",
|
"title": "Cluster",
|
||||||
"version": 0
|
"version": 0
|
||||||
|
|||||||
@@ -19,4 +19,51 @@ weight: 1000
|
|||||||
|
|
||||||
{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
{{< docs/shared lookup="upgrade/upgrade-common-tasks.md" source="grafana" version="<GRAFANA_VERSION>" >}}
|
||||||
|
|
||||||
|
## Ensure that your data source UIDs are following the correct standard
|
||||||
|
|
||||||
|
We have had a standard ways to define UIDs for Grafana objects for years (at least [since Grafana 5](https://github.com/grafana/grafana/issues/7883)). While all of our internal code is complying to this format, we did not yet had strict enforcement of this format in REST APIs and provisioning paths that allow creation and update of data sources.
|
||||||
|
|
||||||
|
In Grafana `11.1` we [introduced](https://github.com/grafana/grafana/pull/86598) a warning that is sent to Grafana server logs every time a data source instance is being created or updated using an invalid UID format.
|
||||||
|
|
||||||
|
In Grafana `11.2` we [added](https://github.com/grafana/grafana/pull/89363/files) a new feature flag called `failWrongDSUID` that is turned off by default. When enabled, the REST APIs and provisioning will start rejecting and requests to create or update datasource instances that have a wrong UID.
|
||||||
|
|
||||||
|
In Grafana `11.5`, we are going to turn feature flag `failWrongDSUID` on by default, but there will still be an option to turn it off.
|
||||||
|
|
||||||
|
In Grafana `12`, this will be the default behavior and will not be configurable.
|
||||||
|
|
||||||
|
### Correct UID format
|
||||||
|
|
||||||
|
You can find the exact regex definition [here](https://github.com/grafana/grafana/blob/c92f5169d1c83508beb777f71a93336179fe426e/pkg/util/shortid_generator.go#L32-L45).
|
||||||
|
|
||||||
|
A data source UID can only contain:
|
||||||
|
|
||||||
|
- latin characters (`a-Z`)
|
||||||
|
- numbers (`0-9`)
|
||||||
|
- dash symbols (`-`)
|
||||||
|
|
||||||
|
### How can I know if I am affected?
|
||||||
|
|
||||||
|
- You can fetch all your data sources via `/api/datasources` API ([docs](https://grafana.com/docs/grafana/latest/developers/http_api/data_source/#get-all-data-sources)). Look into `uid` fields comparing it to the correct format. Below you'll find a script that could help, but please note it is missing authentication that you would [have to add yourself](https://grafana.com/docs/grafana/latest/developers/http_api/#authenticating-api-requests).
|
||||||
|
|
||||||
|
```
|
||||||
|
curl http://localhost:3000/api/datasources | jq '.[] | select((.uid | test("^[a-zA-Z0-9\\-_]+$") | not) or (.uid | length > 40)) | {id, uid, name, type}'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Alternatively, you can check the server logs for the `Invalid datasource uid` ([reference](https://github.com/grafana/grafana/blob/68751ed3107c4d15d33f34b15183ee276611785c/pkg/services/datasources/service/store.go#L429))
|
||||||
|
|
||||||
|
### What can I do if I am affected?
|
||||||
|
|
||||||
|
You will need to create a new data source with the correct UID and update your dashboards and alert rules to use it.
|
||||||
|
|
||||||
|
### How can I update my dashboards to use the new or updated data source?
|
||||||
|
|
||||||
|
1. Go to the dashboard using this data source and update it by selecting the new or updated data source from the picker below your panel.
|
||||||
|
2. Update the dashboard's JSON model directly via search and replace. Navigate to [dashboard json model](https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/view-dashboard-json-model/) and carefully replace all the instances of old `uid` with the newly created `uid`.
|
||||||
|
|
||||||
|
{{< figure src="/media/docs/grafana/screenshot-grafana-11-datasource-uid-enforcement.png" alt="Updating JSON Model of a Dashboard">}}
|
||||||
|
|
||||||
|
### How can I update my alert rules to use the new or updated data source?
|
||||||
|
|
||||||
|
Open the alert rule you want to adjust and search for the data source that is being used for the query/alert condition. From there, select the new data source from the dropdown menu and save the alert rule.
|
||||||
|
|
||||||
## Technical notes
|
## Technical notes
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: num
|
|||||||
e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover
|
e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover
|
||||||
e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click();
|
e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click();
|
||||||
e2e.components.Tab.title('JSON').should('be.visible').click();
|
e2e.components.Tab.title('JSON').should('be.visible').click();
|
||||||
e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click({ force: true });
|
e2e.components.PanelInspector.Json.content().should('be.visible');
|
||||||
|
e2e.components.ReactMonacoEditor.editorLazy().should('be.visible');
|
||||||
|
cy.contains('Panel JSON').click({ force: true });
|
||||||
e2e.components.Select.option().should('be.visible').contains('Panel data').click();
|
e2e.components.Select.option().should('be.visible').contains('Panel data').click();
|
||||||
|
|
||||||
// ensures that panel has loaded without knowingly hitting an error
|
// ensures that panel has loaded without knowingly hitting an error
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ require (
|
|||||||
github.com/grafana/grafana-cloud-migration-snapshot v1.2.0 // @grafana/grafana-operator-experience-squad
|
github.com/grafana/grafana-cloud-migration-snapshot v1.2.0 // @grafana/grafana-operator-experience-squad
|
||||||
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
|
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
|
||||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
|
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0 // @grafana/plugins-platform-backend
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0 // @grafana/plugins-platform-backend
|
||||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 // @grafana/grafana-app-platform-squad
|
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 // @grafana/grafana-app-platform-squad
|
||||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad
|
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad
|
||||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad
|
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad
|
||||||
|
|||||||
@@ -2281,8 +2281,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr
|
|||||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
||||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
|
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0 h1:ZZxHbiiF6QcsnlbPFyZGmzNDoTC1pLeHXUQYoskWt5c=
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0 h1:2KCKA86//O20ffL6WKzHGx5scBbdV7GyEFGnH8Hdv7M=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0/go.mod h1:H3FXrJMUlwocQ6UYj8Ds5I9EzRAVOcdRcgaRE3mXQqk=
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0/go.mod h1:1X8Kgo/SK91Qo1WBCKjPSKrfgjpQys1OkQsHhA78TLg=
|
||||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI=
|
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI=
|
||||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2/go.mod h1:gBLBniiSUQvyt4LRrpIeysj8Many0DV+hdUKifRE0Ec=
|
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2/go.mod h1:gBLBniiSUQvyt4LRrpIeysj8Many0DV+hdUKifRE0Ec=
|
||||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da h1:2E3c/I3ayAy4Z1GwIPqXNZcpUccRapE1aBXA1ho4g7o=
|
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da h1:2E3c/I3ayAy4Z1GwIPqXNZcpUccRapE1aBXA1ho4g7o=
|
||||||
|
|||||||
+11
@@ -545,6 +545,7 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ
|
|||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20220208224320-6efb837e6bc2/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
||||||
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
|
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
|
||||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||||
@@ -622,11 +623,14 @@ github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn
|
|||||||
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
|
||||||
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
|
||||||
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
|
||||||
|
github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s=
|
github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s=
|
||||||
github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||||
|
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||||
|
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||||
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
|
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
|
||||||
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
|
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
|
||||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||||
@@ -638,6 +642,7 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS
|
|||||||
github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8=
|
github.com/fsouza/fake-gcs-server v1.7.0 h1:Un0BXUXrRWYSmYyC1Rqm2e2WJfTPyDy/HGMz31emTi8=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
|
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
@@ -659,6 +664,7 @@ github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4F
|
|||||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs=
|
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs=
|
||||||
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM=
|
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM=
|
||||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
|
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
|
||||||
github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8=
|
github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8=
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -755,6 +761,7 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE=
|
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE=
|
||||||
github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs=
|
github.com/influxdata/influxdb v1.7.6 h1:8mQ7A/V+3noMGCt/P9pD09ISaiz9XvgCk303UYA3gcs=
|
||||||
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig=
|
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig=
|
||||||
|
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
|
||||||
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
|
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
|
||||||
github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
|
github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
|
||||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||||
@@ -967,6 +974,8 @@ github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwy
|
|||||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||||
github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
|
github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
|
||||||
|
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||||
|
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||||
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||||
@@ -1239,6 +1248,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.
|
|||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
|
||||||
@@ -1273,6 +1283,7 @@ gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg
|
|||||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||||
gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs=
|
gopkg.in/telebot.v3 v3.2.1 h1:3I4LohaAyJBiivGmkfB+CiVu7QFOWkuZ4+KHgO/G3rs=
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o=
|
honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o=
|
||||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||||
|
|||||||
+1
-1
@@ -226,7 +226,7 @@
|
|||||||
"sass-loader": "14.2.1",
|
"sass-loader": "14.2.1",
|
||||||
"smtp-tester": "^2.1.0",
|
"smtp-tester": "^2.1.0",
|
||||||
"style-loader": "4.0.0",
|
"style-loader": "4.0.0",
|
||||||
"stylelint": "16.8.2",
|
"stylelint": "16.9.0",
|
||||||
"stylelint-config-sass-guidelines": "11.1.0",
|
"stylelint-config-sass-guidelines": "11.1.0",
|
||||||
"terser-webpack-plugin": "5.3.10",
|
"terser-webpack-plugin": "5.3.10",
|
||||||
"testing-library-selector": "0.3.1",
|
"testing-library-selector": "0.3.1",
|
||||||
|
|||||||
@@ -203,4 +203,8 @@ export interface FeatureToggles {
|
|||||||
dataplaneAggregator?: boolean;
|
dataplaneAggregator?: boolean;
|
||||||
adhocFilterOneOf?: boolean;
|
adhocFilterOneOf?: boolean;
|
||||||
lokiSendDashboardPanelNames?: boolean;
|
lokiSendDashboardPanelNames?: boolean;
|
||||||
|
singleTopNav?: boolean;
|
||||||
|
exploreLogsShardSplitting?: boolean;
|
||||||
|
exploreLogsAggregatedMetrics?: boolean;
|
||||||
|
exploreLogsLimitedTimeRange?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
export interface ScopeDashboardBindingSpec {
|
export interface ScopeDashboardBindingSpec {
|
||||||
dashboard: string;
|
dashboard: string;
|
||||||
dashboardTitle: string;
|
|
||||||
scope: string;
|
scope: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScopeDashboardBindingStatus {
|
||||||
|
dashboardTitle: string;
|
||||||
groups?: string[];
|
groups?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,6 +14,7 @@ export interface ScopeDashboardBinding {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
spec: ScopeDashboardBindingSpec;
|
spec: ScopeDashboardBindingSpec;
|
||||||
|
status: ScopeDashboardBindingStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScopeFilterOperator = 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match';
|
export type ScopeFilterOperator = 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match';
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ import * as common from '@grafana/schema';
|
|||||||
|
|
||||||
export const pluginVersion = "11.3.0-pre";
|
export const pluginVersion = "11.3.0-pre";
|
||||||
|
|
||||||
export interface Options extends common.SingleStatBaseOptions {
|
export interface Options extends common.OptionsWithLegend, common.SingleStatBaseOptions {
|
||||||
displayMode: common.BarGaugeDisplayMode;
|
displayMode: common.BarGaugeDisplayMode;
|
||||||
maxVizHeight: number;
|
maxVizHeight: number;
|
||||||
minVizHeight: number;
|
minVizHeight: number;
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { Combobox, Option } from './Combobox';
|
||||||
|
|
||||||
|
// Mock data for the Combobox options
|
||||||
|
const options: Option[] = [
|
||||||
|
{ label: 'Option 1', value: '1' },
|
||||||
|
{ label: 'Option 2', value: '2' },
|
||||||
|
{ label: 'Option 3', value: '3', description: 'This is option 3' },
|
||||||
|
{ label: 'Option 4', value: '4' },
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('Combobox', () => {
|
||||||
|
const onChangeHandler = jest.fn();
|
||||||
|
beforeAll(() => {
|
||||||
|
const mockGetBoundingClientRect = jest.fn(() => ({
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Object.defineProperty(Element.prototype, 'getBoundingClientRect', {
|
||||||
|
value: mockGetBoundingClientRect,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders without error', () => {
|
||||||
|
render(<Combobox options={options} value={null} onChange={onChangeHandler} />);
|
||||||
|
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow selecting a value by clicking directly', async () => {
|
||||||
|
render(<Combobox options={options} onChange={onChangeHandler} value={null} />);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
userEvent.click(input);
|
||||||
|
|
||||||
|
const item = await screen.findByRole('option', { name: 'Option 1' });
|
||||||
|
await userEvent.click(item);
|
||||||
|
expect(screen.getByDisplayValue('Option 1')).toBeInTheDocument();
|
||||||
|
expect(onChangeHandler).toHaveBeenCalledWith(options[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects value by clicking that needs scrolling', async () => {
|
||||||
|
render(<Combobox options={options} value={null} onChange={onChangeHandler} />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByRole('combobox'));
|
||||||
|
fireEvent.scroll(screen.getByRole('listbox'), { target: { scrollY: 200 } });
|
||||||
|
await userEvent.click(screen.getByText('Option 4'));
|
||||||
|
|
||||||
|
expect(screen.getByDisplayValue('Option 4')).toBeInTheDocument();
|
||||||
|
expect(onChangeHandler).toHaveBeenCalledWith(options[3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects value by searching and pressing enter', async () => {
|
||||||
|
render(<Combobox options={options} value={null} onChange={onChangeHandler} />);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
await userEvent.type(input, 'Option 3');
|
||||||
|
await userEvent.keyboard('{ArrowDown}{Enter}');
|
||||||
|
|
||||||
|
expect(onChangeHandler).toHaveBeenCalledWith(options[2]);
|
||||||
|
expect(screen.getByDisplayValue('Option 3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selects value by using keyboard only', async () => {
|
||||||
|
render(<Combobox options={options} value={null} onChange={onChangeHandler} />);
|
||||||
|
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
await userEvent.click(input);
|
||||||
|
|
||||||
|
await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}');
|
||||||
|
expect(onChangeHandler).toHaveBeenCalledWith(options[1]);
|
||||||
|
expect(screen.queryByDisplayValue('Option 2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears selected value', async () => {
|
||||||
|
render(<Combobox options={options} value={options[1].value} onChange={onChangeHandler} />);
|
||||||
|
|
||||||
|
expect(screen.queryByDisplayValue('Option 2')).toBeInTheDocument();
|
||||||
|
const input = screen.getByRole('combobox');
|
||||||
|
await userEvent.click(input);
|
||||||
|
|
||||||
|
const clearButton = screen.getByTitle('Clear value');
|
||||||
|
await userEvent.click(clearButton);
|
||||||
|
|
||||||
|
expect(onChangeHandler).toHaveBeenCalledWith(null);
|
||||||
|
expect(screen.queryByDisplayValue('Option 2')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -296,7 +296,8 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
mask: css({
|
mask: css({
|
||||||
// The !important here is to override the default .rc-drawer-mask styles
|
// The !important here is to override the default .rc-drawer-mask styles
|
||||||
backgroundColor: 'transparent !important',
|
backgroundColor: 'transparent !important',
|
||||||
position: 'fixed',
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
position: 'fixed !important' as 'fixed',
|
||||||
|
|
||||||
'&:before': {
|
'&:before': {
|
||||||
backgroundColor: `${theme.components.overlay.background} !important`,
|
backgroundColor: `${theme.components.overlay.background} !important`,
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ RadioButtonGroup.displayName = 'RadioButtonGroup';
|
|||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
radioGroup: css({
|
radioGroup: css({
|
||||||
|
backgroundColor: theme.colors.background.primary,
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'nowrap',
|
flexWrap: 'nowrap',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const VIRTUAL_LIST_ITEM_HEIGHT = 37;
|
|||||||
const VIRTUAL_LIST_WIDTH_ESTIMATE_MULTIPLIER = 8;
|
const VIRTUAL_LIST_WIDTH_ESTIMATE_MULTIPLIER = 8;
|
||||||
const VIRTUAL_LIST_PADDING = 8;
|
const VIRTUAL_LIST_PADDING = 8;
|
||||||
// Some list items have icons or checkboxes so we need some extra width
|
// Some list items have icons or checkboxes so we need some extra width
|
||||||
const VIRTUAL_LIST_WIDTH_EXTRA = 36;
|
const VIRTUAL_LIST_WIDTH_EXTRA = 58;
|
||||||
|
|
||||||
// A virtualized version of the SelectMenu, descriptions for SelectableValue options not supported since those are of a variable height.
|
// A virtualized version of the SelectMenu, descriptions for SelectableValue options not supported since those are of a variable height.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { LegendDisplayMode, OptionsWithLegend } from '@grafana/schema';
|
|||||||
*/
|
*/
|
||||||
export function addLegendOptions<T extends OptionsWithLegend>(
|
export function addLegendOptions<T extends OptionsWithLegend>(
|
||||||
builder: PanelOptionsEditorBuilder<T>,
|
builder: PanelOptionsEditorBuilder<T>,
|
||||||
includeLegendCalcs = true
|
includeLegendCalcs = true,
|
||||||
|
showLegend = true
|
||||||
) {
|
) {
|
||||||
builder
|
builder
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
@@ -14,7 +15,7 @@ export function addLegendOptions<T extends OptionsWithLegend>(
|
|||||||
name: 'Visibility',
|
name: 'Visibility',
|
||||||
category: ['Legend'],
|
category: ['Legend'],
|
||||||
description: '',
|
description: '',
|
||||||
defaultValue: true,
|
defaultValue: showLegend,
|
||||||
})
|
})
|
||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'legend.displayMode',
|
path: 'legend.displayMode',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ go 1.23.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/emicklei/go-restful/v3 v3.11.0
|
github.com/emicklei/go-restful/v3 v3.11.0
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0
|
||||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435
|
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435
|
||||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435
|
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435
|
||||||
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
|
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
|
||||||
|
|||||||
@@ -130,8 +130,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0 h1:ZZxHbiiF6QcsnlbPFyZGmzNDoTC1pLeHXUQYoskWt5c=
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0 h1:2KCKA86//O20ffL6WKzHGx5scBbdV7GyEFGnH8Hdv7M=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0/go.mod h1:H3FXrJMUlwocQ6UYj8Ds5I9EzRAVOcdRcgaRE3mXQqk=
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0/go.mod h1:1X8Kgo/SK91Qo1WBCKjPSKrfgjpQys1OkQsHhA78TLg=
|
||||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8=
|
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8=
|
||||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I=
|
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I=
|
||||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA=
|
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA=
|
||||||
|
|||||||
@@ -46,8 +46,11 @@ import (
|
|||||||
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/api")
|
||||||
|
|
||||||
// registerRoutes registers all API HTTP routes.
|
// registerRoutes registers all API HTTP routes.
|
||||||
func (hs *HTTPServer) registerRoutes() {
|
func (hs *HTTPServer) registerRoutes() {
|
||||||
reqNoAuth := middleware.NoAuth()
|
reqNoAuth := middleware.NoAuth()
|
||||||
|
|||||||
+66
-2
@@ -40,6 +40,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (hs *HTTPServer) isDashboardStarredByUser(c *contextmodel.ReqContext, dashID int64) (bool, error) {
|
func (hs *HTTPServer) isDashboardStarredByUser(c *contextmodel.ReqContext, dashID int64) (bool, error) {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.isDashboardStarredByUser")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
if !c.IsSignedIn {
|
if !c.IsSignedIn {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@@ -81,8 +85,9 @@ func dashboardGuardianResponse(err error) response.Response {
|
|||||||
// 404: notFoundError
|
// 404: notFoundError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response {
|
||||||
ctx, span := hs.tracer.Start(c.Req.Context(), "api.GetDashboard")
|
ctx, span := tracer.Start(c.Req.Context(), "api.GetDashboard")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
uid := web.Params(c.Req)[":uid"]
|
uid := web.Params(c.Req)[":uid"]
|
||||||
dash, rsp := hs.getDashboardHelper(ctx, c.SignedInUser.GetOrgID(), 0, uid)
|
dash, rsp := hs.getDashboardHelper(ctx, c.SignedInUser.GetOrgID(), 0, uid)
|
||||||
@@ -230,6 +235,10 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) getAnnotationPermissionsByScope(c *contextmodel.ReqContext, actions *dashboardsV0.AnnotationActions, scope string) {
|
func (hs *HTTPServer) getAnnotationPermissionsByScope(c *contextmodel.ReqContext, actions *dashboardsV0.AnnotationActions, scope string) {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.getAnnotationPermissionsByScope")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope)
|
evaluate := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, scope)
|
||||||
@@ -252,6 +261,9 @@ func (hs *HTTPServer) getAnnotationPermissionsByScope(c *contextmodel.ReqContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string {
|
func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string {
|
||||||
|
ctx, span := tracer.Start(ctx, "api.getUserLogin")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
query := user.GetUserByIDQuery{ID: userID}
|
query := user.GetUserByIDQuery{ID: userID}
|
||||||
user, err := hs.userService.GetByID(ctx, &query)
|
user, err := hs.userService.GetByID(ctx, &query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -292,6 +304,10 @@ func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id in
|
|||||||
// 404: notFoundError
|
// 404: notFoundError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) RestoreDeletedDashboard(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) RestoreDeletedDashboard(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.RestoreDeletedDashboard")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
uid := web.Params(c.Req)[":uid"]
|
uid := web.Params(c.Req)[":uid"]
|
||||||
cmd := dashboards.RestoreDeletedDashboardCommand{}
|
cmd := dashboards.RestoreDeletedDashboardCommand{}
|
||||||
|
|
||||||
@@ -342,6 +358,10 @@ func (hs *HTTPServer) RestoreDeletedDashboard(c *contextmodel.ReqContext) respon
|
|||||||
// 404: notFoundError
|
// 404: notFoundError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) SoftDeleteDashboard(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) SoftDeleteDashboard(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.SoftDeleteDashboard")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
uid := web.Params(c.Req)[":uid"]
|
uid := web.Params(c.Req)[":uid"]
|
||||||
dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, uid)
|
dash, rsp := hs.getDashboardHelper(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, uid)
|
||||||
if rsp != nil {
|
if rsp != nil {
|
||||||
@@ -408,6 +428,10 @@ func (hs *HTTPServer) HardDeleteDashboardByUID(c *contextmodel.ReqContext) respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.deleteDashboard")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
uid := web.Params(c.Req)[":uid"]
|
uid := web.Params(c.Req)[":uid"]
|
||||||
|
|
||||||
var dash *dashboards.Dashboard
|
var dash *dashboards.Dashboard
|
||||||
@@ -496,6 +520,10 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
|
|||||||
// 422: unprocessableEntityError
|
// 422: unprocessableEntityError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) PostDashboard(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) PostDashboard(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.PostDashboard")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
cmd := dashboards.SaveDashboardCommand{}
|
cmd := dashboards.SaveDashboardCommand{}
|
||||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
@@ -504,11 +532,15 @@ func (hs *HTTPServer) PostDashboard(c *contextmodel.ReqContext) response.Respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.SaveDashboardCommand) response.Response {
|
func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.SaveDashboardCommand) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.postDashboard")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
if cmd.IsFolder {
|
if cmd.IsFolder {
|
||||||
return response.Error(http.StatusBadRequest, "Use folders endpoint for saving folders.", nil)
|
return response.Error(http.StatusBadRequest, "Use folders endpoint for saving folders.", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Req.Context()
|
ctx = c.Req.Context()
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
var userID int64
|
var userID int64
|
||||||
@@ -622,6 +654,10 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S
|
|||||||
// 401: unauthorisedError
|
// 401: unauthorisedError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) GetHomeDashboard(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) GetHomeDashboard(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.GetHomeDashboard")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
var userID int64
|
var userID int64
|
||||||
if id, err := identity.UserIdentifier(c.SignedInUser.GetID()); err == nil {
|
if id, err := identity.UserIdentifier(c.SignedInUser.GetID()); err == nil {
|
||||||
userID = id
|
userID = id
|
||||||
@@ -685,6 +721,10 @@ func (hs *HTTPServer) GetHomeDashboard(c *contextmodel.ReqContext) response.Resp
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *contextmodel.ReqContext, dash *simplejson.Json) {
|
func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *contextmodel.ReqContext, dash *simplejson.Json) {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.addGettingStartedPanelToHomeDashboard")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
// We only add this getting started panel for Admins who have not dismissed it,
|
// We only add this getting started panel for Admins who have not dismissed it,
|
||||||
// and if a custom default home dashboard hasn't been configured
|
// and if a custom default home dashboard hasn't been configured
|
||||||
if !c.HasUserRole(org.RoleAdmin) ||
|
if !c.HasUserRole(org.RoleAdmin) ||
|
||||||
@@ -736,6 +776,10 @@ func (hs *HTTPServer) addGettingStartedPanelToHomeDashboard(c *contextmodel.ReqC
|
|||||||
// 404: notFoundError
|
// 404: notFoundError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.GetDashboardVersions")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
var dashID int64
|
var dashID int64
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -846,6 +890,10 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.
|
|||||||
// 404: notFoundError
|
// 404: notFoundError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.GetDashboardVersion")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
var dashID int64
|
var dashID int64
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -921,6 +969,10 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R
|
|||||||
// 403: forbiddenError
|
// 403: forbiddenError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.CalculateDashboardDiff")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
apiOptions := dtos.CalculateDiffOptions{}
|
apiOptions := dtos.CalculateDiffOptions{}
|
||||||
if err := web.Bind(c.Req, &apiOptions); err != nil {
|
if err := web.Bind(c.Req, &apiOptions); err != nil {
|
||||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
@@ -1032,6 +1084,10 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) respons
|
|||||||
// 404: notFoundError
|
// 404: notFoundError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.RestoreDashboardVersion")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
var dashID int64
|
var dashID int64
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@@ -1098,6 +1154,10 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon
|
|||||||
// 401: unauthorisedError
|
// 401: unauthorisedError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) GetDashboardTags(c *contextmodel.ReqContext) {
|
func (hs *HTTPServer) GetDashboardTags(c *contextmodel.ReqContext) {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.GetDashboardTags")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
query := dashboards.GetDashboardTagsQuery{OrgID: c.SignedInUser.GetOrgID()}
|
query := dashboards.GetDashboardTagsQuery{OrgID: c.SignedInUser.GetOrgID()}
|
||||||
queryResult, err := hs.DashboardService.GetDashboardTags(c.Req.Context(), &query)
|
queryResult, err := hs.DashboardService.GetDashboardTags(c.Req.Context(), &query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1110,6 +1170,10 @@ func (hs *HTTPServer) GetDashboardTags(c *contextmodel.ReqContext) {
|
|||||||
|
|
||||||
// GetDashboardUIDs converts internal ids to UIDs
|
// GetDashboardUIDs converts internal ids to UIDs
|
||||||
func (hs *HTTPServer) GetDashboardUIDs(c *contextmodel.ReqContext) {
|
func (hs *HTTPServer) GetDashboardUIDs(c *contextmodel.ReqContext) {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.GetDashboardUIDs")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
ids := strings.Split(web.Params(c.Req)[":ids"], ",")
|
ids := strings.Split(web.Params(c.Req)[":ids"], ",")
|
||||||
uids := make([]string, 0, len(ids))
|
uids := make([]string, 0, len(ids))
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ import (
|
|||||||
// 404: notFoundError
|
// 404: notFoundError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) GetDashboardPermissionList(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) GetDashboardPermissionList(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.GetDashboardPermissionList")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
var dashID int64
|
var dashID int64
|
||||||
var err error
|
var err error
|
||||||
dashUID := web.Params(c.Req)[":uid"]
|
dashUID := web.Params(c.Req)[":uid"]
|
||||||
@@ -117,6 +121,10 @@ func (hs *HTTPServer) GetDashboardPermissionList(c *contextmodel.ReqContext) res
|
|||||||
// 404: notFoundError
|
// 404: notFoundError
|
||||||
// 500: internalServerError
|
// 500: internalServerError
|
||||||
func (hs *HTTPServer) UpdateDashboardPermissions(c *contextmodel.ReqContext) response.Response {
|
func (hs *HTTPServer) UpdateDashboardPermissions(c *contextmodel.ReqContext) response.Response {
|
||||||
|
ctx, span := tracer.Start(c.Req.Context(), "api.UpdateDashboardPermissions")
|
||||||
|
defer span.End()
|
||||||
|
c.Req = c.Req.WithContext(ctx)
|
||||||
|
|
||||||
var dashID int64
|
var dashID int64
|
||||||
var err error
|
var err error
|
||||||
apiCmd := dtos.UpdateDashboardACLCommand{}
|
apiCmd := dtos.UpdateDashboardACLCommand{}
|
||||||
@@ -175,6 +183,9 @@ var dashboardPermissionMap = map[string]dashboardaccess.PermissionType{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) getDashboardACL(ctx context.Context, user identity.Requester, dashboard *dashboards.Dashboard) ([]*dashboards.DashboardACLInfoDTO, error) {
|
func (hs *HTTPServer) getDashboardACL(ctx context.Context, user identity.Requester, dashboard *dashboards.Dashboard) ([]*dashboards.DashboardACLInfoDTO, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "api.getDashboardACL")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
permissions, err := hs.dashboardPermissionsService.GetPermissions(ctx, user, dashboard.UID)
|
permissions, err := hs.dashboardPermissionsService.GetPermissions(ctx, user, dashboard.UID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -253,6 +264,9 @@ func (hs *HTTPServer) filterHiddenACL(user identity.Requester, acl []*dashboards
|
|||||||
|
|
||||||
// updateDashboardAccessControl is used for api backward compatibility
|
// updateDashboardAccessControl is used for api backward compatibility
|
||||||
func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID int64, uid string, isFolder bool, items []*dashboards.DashboardACL, old []*dashboards.DashboardACLInfoDTO) error {
|
func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID int64, uid string, isFolder bool, items []*dashboards.DashboardACL, old []*dashboards.DashboardACLInfoDTO) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "api.updateDashboardAccessControl")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
commands := []accesscontrol.SetResourcePermissionCommand{}
|
commands := []accesscontrol.SetResourcePermissionCommand{}
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
permissions := item.Permission.String()
|
permissions := item.Permission.String()
|
||||||
|
|||||||
+2
-1
@@ -465,7 +465,8 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
|
|||||||
}
|
}
|
||||||
|
|
||||||
compatOpts := plugins.NewCompatOpts(hs.Cfg.BuildVersion, runtime.GOOS, runtime.GOARCH)
|
compatOpts := plugins.NewCompatOpts(hs.Cfg.BuildVersion, runtime.GOOS, runtime.GOARCH)
|
||||||
err := hs.pluginInstaller.Add(c.Req.Context(), pluginID, dto.Version, compatOpts)
|
ctx := repo.WithRequestOrigin(c.Req.Context(), "api")
|
||||||
|
err := hs.pluginInstaller.Add(ctx, pluginID, dto.Version, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dupeErr plugins.DuplicateError
|
var dupeErr plugins.DuplicateError
|
||||||
if errors.As(err, &dupeErr) {
|
if errors.As(err, &dupeErr) {
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ var ScopeDashboardBindingResourceInfo = common.NewResourceInfo(GROUP, VERSION,
|
|||||||
{Name: "Created At", Type: "date"},
|
{Name: "Created At", Type: "date"},
|
||||||
{Name: "Dashboard", Type: "string"},
|
{Name: "Dashboard", Type: "string"},
|
||||||
{Name: "Scope", Type: "string"},
|
{Name: "Scope", Type: "string"},
|
||||||
{Name: "Groups", Type: "array"},
|
|
||||||
},
|
},
|
||||||
Reader: func(obj any) ([]interface{}, error) {
|
Reader: func(obj any) ([]interface{}, error) {
|
||||||
m, ok := obj.(*ScopeDashboardBinding)
|
m, ok := obj.(*ScopeDashboardBinding)
|
||||||
@@ -65,7 +64,6 @@ var ScopeDashboardBindingResourceInfo = common.NewResourceInfo(GROUP, VERSION,
|
|||||||
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
m.CreationTimestamp.UTC().Format(time.RFC3339),
|
||||||
m.Spec.Dashboard,
|
m.Spec.Dashboard,
|
||||||
m.Spec.Scope,
|
m.Spec.Scope,
|
||||||
m.Spec.Groups,
|
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,21 +56,8 @@ type ScopeDashboardBinding struct {
|
|||||||
metav1.TypeMeta `json:",inline"`
|
metav1.TypeMeta `json:",inline"`
|
||||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||||
|
|
||||||
Spec ScopeDashboardBindingSpec `json:"spec,omitempty"`
|
Spec ScopeDashboardBindingSpec `json:"spec,omitempty"`
|
||||||
}
|
Status ScopeDashboardBindingStatus `json:"status,omitempty"`
|
||||||
|
|
||||||
type ScopeDashboardBindingSpec struct {
|
|
||||||
Dashboard string `json:"dashboard"`
|
|
||||||
|
|
||||||
// DashboardTitle should be populated and update from the dashboard
|
|
||||||
DashboardTitle string `json:"dashboardTitle"`
|
|
||||||
|
|
||||||
// Groups is used for the grouping of dashboards that are suggested based
|
|
||||||
// on a scope. The source of truth for this information has not been
|
|
||||||
// determined yet.
|
|
||||||
Groups []string `json:"groups,omitempty"`
|
|
||||||
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
@@ -97,8 +84,36 @@ type ScopeNode struct {
|
|||||||
Spec ScopeNodeSpec `json:"spec,omitempty"`
|
Spec ScopeNodeSpec `json:"spec,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScopeDashboardBindingSpec struct {
|
||||||
|
Dashboard string `json:"dashboard"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
// Type of the item.
|
// Type of the item.
|
||||||
// +enum
|
// +enum
|
||||||
|
// ScopeDashboardBindingStatus contains derived information about a ScopeDashboardBinding.
|
||||||
|
type ScopeDashboardBindingStatus struct {
|
||||||
|
// DashboardTitle should be populated and update from the dashboard
|
||||||
|
DashboardTitle string `json:"dashboardTitle"`
|
||||||
|
|
||||||
|
// Groups is used for the grouping of dashboards that are suggested based
|
||||||
|
// on a scope. The source of truth for this information has not been
|
||||||
|
// determined yet.
|
||||||
|
Groups []string `json:"groups,omitempty"`
|
||||||
|
|
||||||
|
// DashboardTitleConditions is a list of conditions that are used to determine if the dashboard title is valid.
|
||||||
|
// +optional
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=type
|
||||||
|
DashboardTitleConditions []metav1.Condition `json:"dashboardTitleConditions,omitempty"`
|
||||||
|
|
||||||
|
// DashboardTitleConditions is a list of conditions that are used to determine if the list of groups is valid.
|
||||||
|
// +optional
|
||||||
|
// +listType=map
|
||||||
|
// +listMapKey=type
|
||||||
|
GroupsConditions []metav1.Condition `json:"groupsConditions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type NodeType string
|
type NodeType string
|
||||||
|
|
||||||
// Defines values for ItemType.
|
// Defines values for ItemType.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
package v0alpha1
|
package v0alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,7 +109,8 @@ func (in *ScopeDashboardBinding) DeepCopyInto(out *ScopeDashboardBinding) {
|
|||||||
*out = *in
|
*out = *in
|
||||||
out.TypeMeta = in.TypeMeta
|
out.TypeMeta = in.TypeMeta
|
||||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||||
in.Spec.DeepCopyInto(&out.Spec)
|
out.Spec = in.Spec
|
||||||
|
in.Status.DeepCopyInto(&out.Status)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +168,6 @@ func (in *ScopeDashboardBindingList) DeepCopyObject() runtime.Object {
|
|||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *ScopeDashboardBindingSpec) DeepCopyInto(out *ScopeDashboardBindingSpec) {
|
func (in *ScopeDashboardBindingSpec) DeepCopyInto(out *ScopeDashboardBindingSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
if in.Groups != nil {
|
|
||||||
in, out := &in.Groups, &out.Groups
|
|
||||||
*out = make([]string, len(*in))
|
|
||||||
copy(*out, *in)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +181,41 @@ func (in *ScopeDashboardBindingSpec) DeepCopy() *ScopeDashboardBindingSpec {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *ScopeDashboardBindingStatus) DeepCopyInto(out *ScopeDashboardBindingStatus) {
|
||||||
|
*out = *in
|
||||||
|
if in.Groups != nil {
|
||||||
|
in, out := &in.Groups, &out.Groups
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.DashboardTitleConditions != nil {
|
||||||
|
in, out := &in.DashboardTitleConditions, &out.DashboardTitleConditions
|
||||||
|
*out = make([]v1.Condition, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if in.GroupsConditions != nil {
|
||||||
|
in, out := &in.GroupsConditions, &out.GroupsConditions
|
||||||
|
*out = make([]v1.Condition, len(*in))
|
||||||
|
for i := range *in {
|
||||||
|
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScopeDashboardBindingStatus.
|
||||||
|
func (in *ScopeDashboardBindingStatus) DeepCopy() *ScopeDashboardBindingStatus {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(ScopeDashboardBindingStatus)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *ScopeFilter) DeepCopyInto(out *ScopeFilter) {
|
func (in *ScopeFilter) DeepCopyInto(out *ScopeFilter) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
|||||||
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBinding": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBinding(ref),
|
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBinding": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBinding(ref),
|
||||||
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingList": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingList(ref),
|
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingList": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingList(ref),
|
||||||
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingSpec(ref),
|
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingSpec(ref),
|
||||||
|
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus": schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingStatus(ref),
|
||||||
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeFilter": schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref),
|
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeFilter": schema_pkg_apis_scope_v0alpha1_ScopeFilter(ref),
|
||||||
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeList": schema_pkg_apis_scope_v0alpha1_ScopeList(ref),
|
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeList": schema_pkg_apis_scope_v0alpha1_ScopeList(ref),
|
||||||
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNode": schema_pkg_apis_scope_v0alpha1_ScopeNode(ref),
|
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeNode": schema_pkg_apis_scope_v0alpha1_ScopeNode(ref),
|
||||||
@@ -195,11 +196,17 @@ func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBinding(ref common.ReferenceCa
|
|||||||
Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec"),
|
Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"status": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: map[string]interface{}{},
|
||||||
|
Ref: ref("github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Dependencies: []string{
|
Dependencies: []string{
|
||||||
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
"github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingSpec", "github.com/grafana/grafana/pkg/apis/scope/v0alpha1.ScopeDashboardBindingStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +270,27 @@ func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingSpec(ref common.Referen
|
|||||||
Format: "",
|
Format: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scope": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: "",
|
||||||
|
Type: []string{"string"},
|
||||||
|
Format: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Required: []string{"dashboard", "scope"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||||
|
return common.OpenAPIDefinition{
|
||||||
|
Schema: spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "Type of the item. ScopeDashboardBindingStatus contains derived information about a ScopeDashboardBinding.",
|
||||||
|
Type: []string{"object"},
|
||||||
|
Properties: map[string]spec.Schema{
|
||||||
"dashboardTitle": {
|
"dashboardTitle": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "DashboardTitle should be populated and update from the dashboard",
|
Description: "DashboardTitle should be populated and update from the dashboard",
|
||||||
@@ -286,17 +314,56 @@ func schema_pkg_apis_scope_v0alpha1_ScopeDashboardBindingSpec(ref common.Referen
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"scope": {
|
"dashboardTitleConditions": {
|
||||||
|
VendorExtensible: spec.VendorExtensible{
|
||||||
|
Extensions: spec.Extensions{
|
||||||
|
"x-kubernetes-list-map-keys": []interface{}{
|
||||||
|
"type",
|
||||||
|
},
|
||||||
|
"x-kubernetes-list-type": "map",
|
||||||
|
},
|
||||||
|
},
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Default: "",
|
Description: "DashboardTitleConditions is a list of conditions that are used to determine if the dashboard title is valid.",
|
||||||
Type: []string{"string"},
|
Type: []string{"array"},
|
||||||
Format: "",
|
Items: &spec.SchemaOrArray{
|
||||||
|
Schema: &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: map[string]interface{}{},
|
||||||
|
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"groupsConditions": {
|
||||||
|
VendorExtensible: spec.VendorExtensible{
|
||||||
|
Extensions: spec.Extensions{
|
||||||
|
"x-kubernetes-list-map-keys": []interface{}{
|
||||||
|
"type",
|
||||||
|
},
|
||||||
|
"x-kubernetes-list-type": "map",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "DashboardTitleConditions is a list of conditions that are used to determine if the list of groups is valid.",
|
||||||
|
Type: []string{"array"},
|
||||||
|
Items: &spec.SchemaOrArray{
|
||||||
|
Schema: &spec.Schema{
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Default: map[string]interface{}{},
|
||||||
|
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Required: []string{"dashboard", "dashboardTitle", "scope"},
|
Required: []string{"dashboardTitle"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Dependencies: []string{
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1.Condition"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,11 +551,9 @@ func schema_pkg_apis_scope_v0alpha1_ScopeNodeSpec(ref common.ReferenceCallback)
|
|||||||
},
|
},
|
||||||
"nodeType": {
|
"nodeType": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "Possible enum values:\n - `\"container\"`\n - `\"leaf\"`",
|
Default: "",
|
||||||
Default: "",
|
Type: []string{"string"},
|
||||||
Type: []string{"string"},
|
Format: "",
|
||||||
Format: "",
|
|
||||||
Enum: []interface{}{"container", "leaf"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,FindScopeDashboardBindingsResults,Items
|
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,FindScopeDashboardBindingsResults,Items
|
||||||
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboardBindingSpec,Groups
|
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeDashboardBindingStatus,Groups
|
||||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeNodeSpec,LinkID
|
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/scope/v0alpha1,ScopeNodeSpec,LinkID
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ type DualWriterMode int
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// Mode0 represents writing to and reading from solely LegacyStorage. This mode is enabled when the
|
// Mode0 represents writing to and reading from solely LegacyStorage. This mode is enabled when the
|
||||||
// `unifiedStorage` feature flag is not set. All reads and writes are made to LegacyStorage. None are made to Storage.
|
// Unified Storage is disabled. All reads and writes are made to LegacyStorage. None are made to Storage.
|
||||||
Mode0 DualWriterMode = iota
|
Mode0 DualWriterMode = iota
|
||||||
// Mode1 represents writing to and reading from LegacyStorage for all primary functionality while additionally
|
// Mode1 represents writing to and reading from LegacyStorage for all primary functionality while additionally
|
||||||
// reading and writing to Storage on a best effort basis for the sake of collecting metrics.
|
// reading and writing to Storage on a best effort basis for the sake of collecting metrics.
|
||||||
@@ -110,27 +110,27 @@ func NewDualWriter(
|
|||||||
legacy LegacyStorage,
|
legacy LegacyStorage,
|
||||||
storage Storage,
|
storage Storage,
|
||||||
reg prometheus.Registerer,
|
reg prometheus.Registerer,
|
||||||
kind string,
|
resource string,
|
||||||
) DualWriter {
|
) DualWriter {
|
||||||
metrics := &dualWriterMetrics{}
|
metrics := &dualWriterMetrics{}
|
||||||
metrics.init(reg)
|
metrics.init(reg)
|
||||||
switch mode {
|
switch mode {
|
||||||
// It is not possible to initialize a mode 0 dual writer. Mode 0 represents
|
// It is not possible to initialize a mode 0 dual writer. Mode 0 represents
|
||||||
// writing to legacy storage without `unifiedStorage` enabled.
|
// writing to legacy storage without Unified Storage enabled.
|
||||||
case Mode1:
|
case Mode1:
|
||||||
// read and write only from legacy storage
|
// read and write only from legacy storage
|
||||||
return newDualWriterMode1(legacy, storage, metrics, kind)
|
return newDualWriterMode1(legacy, storage, metrics, resource)
|
||||||
case Mode2:
|
case Mode2:
|
||||||
// write to both, read from storage but use legacy as backup
|
// write to both, read from storage but use legacy as backup
|
||||||
return newDualWriterMode2(legacy, storage, metrics, kind)
|
return newDualWriterMode2(legacy, storage, metrics, resource)
|
||||||
case Mode3:
|
case Mode3:
|
||||||
// write to both, read from storage only
|
// write to both, read from storage only
|
||||||
return newDualWriterMode3(legacy, storage, metrics, kind)
|
return newDualWriterMode3(legacy, storage, metrics, resource)
|
||||||
case Mode4:
|
case Mode4:
|
||||||
// read and write only from storage
|
// read and write only from storage
|
||||||
return newDualWriterMode4(legacy, storage, metrics, kind)
|
return newDualWriterMode4(legacy, storage, metrics, resource)
|
||||||
default:
|
default:
|
||||||
return newDualWriterMode1(legacy, storage, metrics, kind)
|
return newDualWriterMode1(legacy, storage, metrics, resource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ func SetDualWritingMode(
|
|||||||
|
|
||||||
toMode := map[string]DualWriterMode{
|
toMode := map[string]DualWriterMode{
|
||||||
// It is not possible to initialize a mode 0 dual writer. Mode 0 represents
|
// It is not possible to initialize a mode 0 dual writer. Mode 0 represents
|
||||||
// writing to legacy storage without `unifiedStorage` enabled.
|
// writing to legacy storage without Unified Storage enabled.
|
||||||
"1": Mode1,
|
"1": Mode1,
|
||||||
"2": Mode2,
|
"2": Mode2,
|
||||||
"3": Mode3,
|
"3": Mode3,
|
||||||
|
|||||||
@@ -18,16 +18,22 @@ type DualWriterMode1 struct {
|
|||||||
Legacy LegacyStorage
|
Legacy LegacyStorage
|
||||||
Storage Storage
|
Storage Storage
|
||||||
*dualWriterMetrics
|
*dualWriterMetrics
|
||||||
kind string
|
resource string
|
||||||
Log klog.Logger
|
Log klog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode1Str = "1"
|
const mode1Str = "1"
|
||||||
|
|
||||||
// NewDualWriterMode1 returns a new DualWriter in mode 1.
|
// NewDualWriterMode1 returns a new DualWriter in mode 1.
|
||||||
// Mode 1 represents writing to and reading from LegacyStorage.
|
// Mode 1 represents writing to and reading from LegacyStorage.
|
||||||
func newDualWriterMode1(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, kind string) *DualWriterMode1 {
|
func newDualWriterMode1(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode1 {
|
||||||
return &DualWriterMode1{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode1").WithValues("mode", mode1Str, "kind", kind), dualWriterMetrics: dwm}
|
return &DualWriterMode1{
|
||||||
|
Legacy: legacy,
|
||||||
|
Storage: storage,
|
||||||
|
Log: klog.NewKlogr().WithName("DualWriterMode1").WithValues("mode", mode1Str, "resource", resource),
|
||||||
|
dualWriterMetrics: dwm,
|
||||||
|
resource: resource,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode returns the mode of the dual writer.
|
// Mode returns the mode of the dual writer.
|
||||||
@@ -45,10 +51,10 @@ func (d *DualWriterMode1) Create(ctx context.Context, original runtime.Object, c
|
|||||||
created, err := d.Legacy.Create(ctx, original, createValidation, options)
|
created, err := d.Legacy.Create(ctx, original, createValidation, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to create object in legacy storage")
|
log.Error(err, "unable to create object in legacy storage")
|
||||||
d.recordLegacyDuration(true, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode1Str, d.resource, method, startLegacy)
|
||||||
return created, err
|
return created, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(false, mode1Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
createdCopy := created.DeepCopyObject()
|
createdCopy := created.DeepCopyObject()
|
||||||
|
|
||||||
@@ -62,7 +68,7 @@ func (d *DualWriterMode1) Create(ctx context.Context, original runtime.Object, c
|
|||||||
|
|
||||||
startStorage := time.Now()
|
startStorage := time.Now()
|
||||||
storageObj, errObjectSt := d.Storage.Create(ctx, createdCopy, createValidation, options)
|
storageObj, errObjectSt := d.Storage.Create(ctx, createdCopy, createValidation, options)
|
||||||
d.recordStorageDuration(errObjectSt != nil, mode1Str, d.kind, method, startStorage)
|
d.recordStorageDuration(errObjectSt != nil, mode1Str, d.resource, method, startStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
@@ -87,14 +93,14 @@ func (d *DualWriterMode1) Get(ctx context.Context, name string, options *metav1.
|
|||||||
if errLegacy != nil {
|
if errLegacy != nil {
|
||||||
log.Error(errLegacy, "unable to get object in legacy storage")
|
log.Error(errLegacy, "unable to get object in legacy storage")
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(errLegacy != nil, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(errLegacy != nil, mode1Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
go func(res runtime.Object) {
|
go func(res runtime.Object) {
|
||||||
startStorage := time.Now()
|
startStorage := time.Now()
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage get timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage get timeout"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
storageObj, err := d.Storage.Get(ctx, name, options)
|
storageObj, err := d.Storage.Get(ctx, name, options)
|
||||||
d.recordStorageDuration(err != nil, mode1Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode1Str, d.resource, method, startStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to get object in storage")
|
log.Error(err, "unable to get object in storage")
|
||||||
cancel()
|
cancel()
|
||||||
@@ -121,14 +127,14 @@ func (d *DualWriterMode1) List(ctx context.Context, options *metainternalversion
|
|||||||
if errLegacy != nil {
|
if errLegacy != nil {
|
||||||
log.Error(errLegacy, "unable to list object in legacy storage")
|
log.Error(errLegacy, "unable to list object in legacy storage")
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(errLegacy != nil, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(errLegacy != nil, mode1Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
go func(res runtime.Object) {
|
go func(res runtime.Object) {
|
||||||
startStorage := time.Now()
|
startStorage := time.Now()
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage list timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage list timeout"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
storageObj, err := d.Storage.List(ctx, options)
|
storageObj, err := d.Storage.List(ctx, options)
|
||||||
d.recordStorageDuration(err != nil, mode1Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode1Str, d.resource, method, startStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
@@ -151,7 +157,7 @@ func (d *DualWriterMode1) Delete(ctx context.Context, name string, deleteValidat
|
|||||||
res, async, err := d.Legacy.Delete(ctx, name, deleteValidation, options)
|
res, async, err := d.Legacy.Delete(ctx, name, deleteValidation, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to delete object in legacy storage")
|
log.Error(err, "unable to delete object in legacy storage")
|
||||||
d.recordLegacyDuration(true, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode1Str, d.resource, method, startLegacy)
|
||||||
return res, async, err
|
return res, async, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode1Str, name, method, startLegacy)
|
d.recordLegacyDuration(false, mode1Str, name, method, startLegacy)
|
||||||
@@ -161,7 +167,7 @@ func (d *DualWriterMode1) Delete(ctx context.Context, name string, deleteValidat
|
|||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage delete timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage delete timeout"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
storageObj, _, err := d.Storage.Delete(ctx, name, deleteValidation, options)
|
storageObj, _, err := d.Storage.Delete(ctx, name, deleteValidation, options)
|
||||||
d.recordStorageDuration(err != nil, mode1Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode1Str, d.resource, method, startStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
@@ -185,17 +191,17 @@ func (d *DualWriterMode1) DeleteCollection(ctx context.Context, deleteValidation
|
|||||||
res, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
res, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to delete collection in legacy storage")
|
log.Error(err, "unable to delete collection in legacy storage")
|
||||||
d.recordLegacyDuration(true, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode1Str, d.resource, method, startLegacy)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(false, mode1Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
go func(res runtime.Object) {
|
go func(res runtime.Object) {
|
||||||
startStorage := time.Now()
|
startStorage := time.Now()
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage deletecollection timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage deletecollection timeout"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
storageObj, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
storageObj, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
||||||
d.recordStorageDuration(err != nil, mode1Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode1Str, d.resource, method, startStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
@@ -218,10 +224,10 @@ func (d *DualWriterMode1) Update(ctx context.Context, name string, objInfo rest.
|
|||||||
res, async, err := d.Legacy.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
res, async, err := d.Legacy.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to update in legacy storage")
|
log.Error(err, "unable to update in legacy storage")
|
||||||
d.recordLegacyDuration(true, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode1Str, d.resource, method, startLegacy)
|
||||||
return res, async, err
|
return res, async, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode1Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(false, mode1Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
go func(res runtime.Object) {
|
go func(res runtime.Object) {
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage update timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("storage update timeout"))
|
||||||
@@ -257,7 +263,7 @@ func (d *DualWriterMode1) Update(ctx context.Context, name string, objInfo rest.
|
|||||||
startStorage := time.Now()
|
startStorage := time.Now()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
storageObj, _, errObjectSt := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
storageObj, _, errObjectSt := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||||
d.recordStorageDuration(errObjectSt != nil, mode1Str, d.kind, method, startStorage)
|
d.recordStorageDuration(errObjectSt != nil, mode1Str, d.resource, method, startStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,17 +26,21 @@ type DualWriterMode2 struct {
|
|||||||
Storage Storage
|
Storage Storage
|
||||||
Legacy LegacyStorage
|
Legacy LegacyStorage
|
||||||
*dualWriterMetrics
|
*dualWriterMetrics
|
||||||
kind string
|
resource string
|
||||||
Log klog.Logger
|
Log klog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode2Str = "2"
|
const mode2Str = "2"
|
||||||
|
|
||||||
// NewDualWriterMode2 returns a new DualWriter in mode 2.
|
// NewDualWriterMode2 returns a new DualWriter in mode 2.
|
||||||
// Mode 2 represents writing to LegacyStorage and Storage and reading from LegacyStorage.
|
// Mode 2 represents writing to LegacyStorage and Storage and reading from LegacyStorage.
|
||||||
func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, kind string) *DualWriterMode2 {
|
func newDualWriterMode2(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode2 {
|
||||||
return &DualWriterMode2{
|
return &DualWriterMode2{
|
||||||
Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode2").WithValues("mode", mode2Str, "kind", kind), dualWriterMetrics: dwm,
|
Legacy: legacy,
|
||||||
|
Storage: storage,
|
||||||
|
Log: klog.NewKlogr().WithName("DualWriterMode2").WithValues("mode", mode2Str, "resource", resource),
|
||||||
|
dualWriterMetrics: dwm,
|
||||||
|
resource: resource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +59,10 @@ func (d *DualWriterMode2) Create(ctx context.Context, original runtime.Object, c
|
|||||||
created, err := d.Legacy.Create(ctx, original, createValidation, options)
|
created, err := d.Legacy.Create(ctx, original, createValidation, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to create object in legacy storage")
|
log.Error(err, "unable to create object in legacy storage")
|
||||||
d.recordLegacyDuration(true, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy)
|
||||||
return created, err
|
return created, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
if err := enrichLegacyObject(original, created); err != nil {
|
if err := enrichLegacyObject(original, created); err != nil {
|
||||||
return created, err
|
return created, err
|
||||||
@@ -68,10 +72,10 @@ func (d *DualWriterMode2) Create(ctx context.Context, original runtime.Object, c
|
|||||||
rsp, err := d.Storage.Create(ctx, created, createValidation, options)
|
rsp, err := d.Storage.Create(ctx, created, createValidation, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithValues("name").Error(err, "unable to create object in storage")
|
log.WithValues("name").Error(err, "unable to create object in storage")
|
||||||
d.recordStorageDuration(true, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage)
|
||||||
return rsp, err
|
return rsp, err
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(false, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
areEqual := Compare(rsp, created)
|
areEqual := Compare(rsp, created)
|
||||||
d.recordOutcome(mode2Str, getName(rsp), areEqual, method)
|
d.recordOutcome(mode2Str, getName(rsp), areEqual, method)
|
||||||
@@ -89,7 +93,7 @@ func (d *DualWriterMode2) Get(ctx context.Context, name string, options *metav1.
|
|||||||
|
|
||||||
startStorage := time.Now()
|
startStorage := time.Now()
|
||||||
objStorage, err := d.Storage.Get(ctx, name, options)
|
objStorage, err := d.Storage.Get(ctx, name, options)
|
||||||
d.recordStorageDuration(err != nil, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode2Str, d.resource, method, startStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// if it errors because it's not found, we try to fetch it from the legacy storage
|
// if it errors because it's not found, we try to fetch it from the legacy storage
|
||||||
if !apierrors.IsNotFound(err) {
|
if !apierrors.IsNotFound(err) {
|
||||||
@@ -103,10 +107,10 @@ func (d *DualWriterMode2) Get(ctx context.Context, name string, options *metav1.
|
|||||||
objLegacy, err := d.Legacy.Get(ctx, name, options)
|
objLegacy, err := d.Legacy.Get(ctx, name, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to fetch object from legacy")
|
log.Error(err, "unable to fetch object from legacy")
|
||||||
d.recordLegacyDuration(true, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy)
|
||||||
return objLegacy, err
|
return objLegacy, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
areEqual := Compare(objStorage, objLegacy)
|
areEqual := Compare(objStorage, objLegacy)
|
||||||
d.recordOutcome(mode2Str, name, areEqual, method)
|
d.recordOutcome(mode2Str, name, areEqual, method)
|
||||||
@@ -132,10 +136,10 @@ func (d *DualWriterMode2) List(ctx context.Context, options *metainternalversion
|
|||||||
ll, err := d.Legacy.List(ctx, options)
|
ll, err := d.Legacy.List(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to list objects from legacy storage")
|
log.Error(err, "unable to list objects from legacy storage")
|
||||||
d.recordLegacyDuration(true, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy)
|
||||||
return ll, err
|
return ll, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
legacyList, err := meta.ExtractList(ll)
|
legacyList, err := meta.ExtractList(ll)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -154,10 +158,10 @@ func (d *DualWriterMode2) List(ctx context.Context, options *metainternalversion
|
|||||||
sl, err := d.Storage.List(ctx, options)
|
sl, err := d.Storage.List(ctx, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to list objects from storage")
|
log.Error(err, "unable to list objects from storage")
|
||||||
d.recordStorageDuration(true, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage)
|
||||||
return sl, err
|
return sl, err
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(false, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
storageList, err := meta.ExtractList(sl)
|
storageList, err := meta.ExtractList(sl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -181,11 +185,7 @@ func (d *DualWriterMode2) List(ctx context.Context, options *metainternalversion
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the number of items in the legacy list and the storage list are the same, we can return the storage list
|
// always return the list from legacy storage
|
||||||
if len(storageList) == len(legacyList) {
|
|
||||||
return sl, nil
|
|
||||||
}
|
|
||||||
log.Info("lists from legacy and storage are not the same size")
|
|
||||||
return ll, nil
|
return ll, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,10 +199,10 @@ func (d *DualWriterMode2) DeleteCollection(ctx context.Context, deleteValidation
|
|||||||
deleted, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
deleted, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithValues("deleted", deleted).Error(err, "failed to delete collection successfully from legacy storage")
|
log.WithValues("deleted", deleted).Error(err, "failed to delete collection successfully from legacy storage")
|
||||||
d.recordLegacyDuration(true, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy)
|
||||||
return deleted, err
|
return deleted, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
legacyList, err := meta.ExtractList(deleted)
|
legacyList, err := meta.ExtractList(deleted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -220,10 +220,10 @@ func (d *DualWriterMode2) DeleteCollection(ctx context.Context, deleteValidation
|
|||||||
res, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
res, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithValues("deleted", res).Error(err, "failed to delete collection successfully from Storage")
|
log.WithValues("deleted", res).Error(err, "failed to delete collection successfully from Storage")
|
||||||
d.recordStorageDuration(true, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(false, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
areEqual := Compare(res, deleted)
|
areEqual := Compare(res, deleted)
|
||||||
d.recordOutcome(mode2Str, getName(res), areEqual, method)
|
d.recordOutcome(mode2Str, getName(res), areEqual, method)
|
||||||
@@ -245,22 +245,22 @@ func (d *DualWriterMode2) Delete(ctx context.Context, name string, deleteValidat
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if !apierrors.IsNotFound(err) {
|
if !apierrors.IsNotFound(err) {
|
||||||
log.WithValues("objectList", deletedLS).Error(err, "could not delete from legacy store")
|
log.WithValues("objectList", deletedLS).Error(err, "could not delete from legacy store")
|
||||||
d.recordLegacyDuration(true, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(true, mode2Str, d.resource, method, startLegacy)
|
||||||
return deletedLS, async, err
|
return deletedLS, async, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode2Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(false, mode2Str, d.resource, method, startLegacy)
|
||||||
|
|
||||||
startStorage := time.Now()
|
startStorage := time.Now()
|
||||||
deletedS, _, err := d.Storage.Delete(ctx, name, deleteValidation, options)
|
deletedS, _, err := d.Storage.Delete(ctx, name, deleteValidation, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !apierrors.IsNotFound(err) {
|
if !apierrors.IsNotFound(err) {
|
||||||
log.WithValues("objectList", deletedS).Error(err, "could not delete from duplicate storage")
|
log.WithValues("objectList", deletedS).Error(err, "could not delete from duplicate storage")
|
||||||
d.recordStorageDuration(true, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(true, mode2Str, d.resource, method, startStorage)
|
||||||
}
|
}
|
||||||
return deletedS, async, err
|
return deletedS, async, err
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(false, mode2Str, d.kind, method, startStorage)
|
d.recordStorageDuration(false, mode2Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
areEqual := Compare(deletedS, deletedLS)
|
areEqual := Compare(deletedS, deletedLS)
|
||||||
d.recordOutcome(mode2Str, name, areEqual, method)
|
d.recordOutcome(mode2Str, name, areEqual, method)
|
||||||
@@ -298,10 +298,10 @@ func (d *DualWriterMode2) Update(ctx context.Context, name string, objInfo rest.
|
|||||||
obj, created, err := d.Legacy.Update(ctx, name, &updateWrapper{upstream: objInfo, updated: updated}, createValidation, updateValidation, forceAllowCreate, options)
|
obj, created, err := d.Legacy.Update(ctx, name, &updateWrapper{upstream: objInfo, updated: updated}, createValidation, updateValidation, forceAllowCreate, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithValues("object", obj).Error(err, "could not update in legacy storage")
|
log.WithValues("object", obj).Error(err, "could not update in legacy storage")
|
||||||
d.recordLegacyDuration(true, mode2Str, d.kind, "update", startLegacy)
|
d.recordLegacyDuration(true, mode2Str, d.resource, "update", startLegacy)
|
||||||
return obj, created, err
|
return obj, created, err
|
||||||
}
|
}
|
||||||
d.recordLegacyDuration(false, mode2Str, d.kind, "update", startLegacy)
|
d.recordLegacyDuration(false, mode2Str, d.resource, "update", startLegacy)
|
||||||
|
|
||||||
// if the object is found, create a new updateWrapper with the object found
|
// if the object is found, create a new updateWrapper with the object found
|
||||||
if foundObj != nil {
|
if foundObj != nil {
|
||||||
@@ -320,7 +320,7 @@ func (d *DualWriterMode2) Update(ctx context.Context, name string, objInfo rest.
|
|||||||
res, created, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
res, created, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithValues("object", res).Error(err, "could not update in storage")
|
log.WithValues("object", res).Error(err, "could not update in storage")
|
||||||
d.recordStorageDuration(true, mode2Str, d.kind, "update", startStorage)
|
d.recordStorageDuration(true, mode2Str, d.resource, "update", startStorage)
|
||||||
return res, created, err
|
return res, created, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +434,7 @@ func getList(ctx context.Context, obj rest.Lister, listOptions *metainternalvers
|
|||||||
return meta.ExtractList(ll)
|
return meta.ExtractList(ll)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mode2DataSyncer(ctx context.Context, legacy LegacyStorage, storage Storage, kind string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) {
|
func mode2DataSyncer(ctx context.Context, legacy LegacyStorage, storage Storage, resource string, reg prometheus.Registerer, serverLockService ServerLockService, requestInfo *request.RequestInfo) (bool, error) {
|
||||||
metrics := &dualWriterMetrics{}
|
metrics := &dualWriterMetrics{}
|
||||||
metrics.init(reg)
|
metrics.init(reg)
|
||||||
|
|
||||||
@@ -599,8 +599,8 @@ func mode2DataSyncer(ctx context.Context, legacy LegacyStorage, storage Storage,
|
|||||||
|
|
||||||
everythingSynced = outOfSync == syncSuccess
|
everythingSynced = outOfSync == syncSuccess
|
||||||
|
|
||||||
metrics.recordDataSyncerOutcome(mode2Str, kind, everythingSynced)
|
metrics.recordDataSyncerOutcome(mode2Str, resource, everythingSynced)
|
||||||
metrics.recordDataSyncerDuration(err != nil, mode2Str, kind, startSync)
|
metrics.recordDataSyncerDuration(err != nil, mode2Str, resource, startSync)
|
||||||
|
|
||||||
log.Info("finished syncing items", "items", len(itemsByName), "updated", syncSuccess, "failed", syncErr, "outcome", everythingSynced)
|
log.Info("finished syncing items", "items", len(itemsByName), "updated", syncSuccess, "failed", syncErr, "outcome", everythingSynced)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,14 +18,20 @@ type DualWriterMode3 struct {
|
|||||||
Storage Storage
|
Storage Storage
|
||||||
watchImp rest.Watcher // watch is only available in mode 3 and 4
|
watchImp rest.Watcher // watch is only available in mode 3 and 4
|
||||||
*dualWriterMetrics
|
*dualWriterMetrics
|
||||||
kind string
|
resource string
|
||||||
Log klog.Logger
|
Log klog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDualWriterMode3 returns a new DualWriter in mode 3.
|
// newDualWriterMode3 returns a new DualWriter in mode 3.
|
||||||
// Mode 3 represents writing to LegacyStorage and Storage and reading from Storage.
|
// Mode 3 represents writing to LegacyStorage and Storage and reading from Storage.
|
||||||
func newDualWriterMode3(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, kind string) *DualWriterMode3 {
|
func newDualWriterMode3(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode3 {
|
||||||
return &DualWriterMode3{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode3").WithValues("mode", mode3Str, "kind", kind), dualWriterMetrics: dwm}
|
return &DualWriterMode3{
|
||||||
|
Legacy: legacy,
|
||||||
|
Storage: storage,
|
||||||
|
Log: klog.NewKlogr().WithName("DualWriterMode3").WithValues("mode", mode3Str, "resource", resource),
|
||||||
|
dualWriterMetrics: dwm,
|
||||||
|
resource: resource,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode returns the mode of the dual writer.
|
// Mode returns the mode of the dual writer.
|
||||||
@@ -45,10 +51,10 @@ func (d *DualWriterMode3) Create(ctx context.Context, obj runtime.Object, create
|
|||||||
created, err := d.Storage.Create(ctx, obj, createValidation, options)
|
created, err := d.Storage.Create(ctx, obj, createValidation, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to create object in storage")
|
log.Error(err, "unable to create object in storage")
|
||||||
d.recordLegacyDuration(true, mode3Str, d.kind, method, startStorage)
|
d.recordLegacyDuration(true, mode3Str, d.resource, method, startStorage)
|
||||||
return created, err
|
return created, err
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(false, mode3Str, d.kind, method, startStorage)
|
d.recordStorageDuration(false, mode3Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy create timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy create timeout"))
|
||||||
@@ -56,7 +62,7 @@ func (d *DualWriterMode3) Create(ctx context.Context, obj runtime.Object, create
|
|||||||
|
|
||||||
startLegacy := time.Now()
|
startLegacy := time.Now()
|
||||||
_, errObjectSt := d.Legacy.Create(ctx, obj, createValidation, options)
|
_, errObjectSt := d.Legacy.Create(ctx, obj, createValidation, options)
|
||||||
d.recordLegacyDuration(errObjectSt != nil, mode3Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(errObjectSt != nil, mode3Str, d.resource, method, startLegacy)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return created, err
|
return created, err
|
||||||
@@ -73,7 +79,7 @@ func (d *DualWriterMode3) Get(ctx context.Context, name string, options *metav1.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to get object in storage")
|
log.Error(err, "unable to get object in storage")
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(err != nil, mode3Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode3Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
@@ -89,7 +95,7 @@ func (d *DualWriterMode3) List(ctx context.Context, options *metainternalversion
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to list object in storage")
|
log.Error(err, "unable to list object in storage")
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(err != nil, mode3Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode3Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
@@ -103,7 +109,7 @@ func (d *DualWriterMode3) Delete(ctx context.Context, name string, deleteValidat
|
|||||||
res, async, err := d.Storage.Delete(ctx, name, deleteValidation, options)
|
res, async, err := d.Storage.Delete(ctx, name, deleteValidation, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to delete object in storage")
|
log.Error(err, "unable to delete object in storage")
|
||||||
d.recordStorageDuration(true, mode3Str, d.kind, method, startStorage)
|
d.recordStorageDuration(true, mode3Str, d.resource, method, startStorage)
|
||||||
return res, async, err
|
return res, async, err
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(false, mode3Str, name, method, startStorage)
|
d.recordStorageDuration(false, mode3Str, name, method, startStorage)
|
||||||
@@ -113,7 +119,7 @@ func (d *DualWriterMode3) Delete(ctx context.Context, name string, deleteValidat
|
|||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy delete timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy delete timeout"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, _, err := d.Legacy.Delete(ctx, name, deleteValidation, options)
|
_, _, err := d.Legacy.Delete(ctx, name, deleteValidation, options)
|
||||||
d.recordLegacyDuration(err != nil, mode3Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(err != nil, mode3Str, d.resource, method, startLegacy)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return res, async, err
|
return res, async, err
|
||||||
@@ -129,10 +135,10 @@ func (d *DualWriterMode3) Update(ctx context.Context, name string, objInfo rest.
|
|||||||
res, async, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
res, async, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to update in storage")
|
log.Error(err, "unable to update in storage")
|
||||||
d.recordLegacyDuration(true, mode3Str, d.kind, method, startStorage)
|
d.recordLegacyDuration(true, mode3Str, d.resource, method, startStorage)
|
||||||
return res, async, err
|
return res, async, err
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(false, mode3Str, d.kind, method, startStorage)
|
d.recordStorageDuration(false, mode3Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy update timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy update timeout"))
|
||||||
@@ -140,7 +146,7 @@ func (d *DualWriterMode3) Update(ctx context.Context, name string, objInfo rest.
|
|||||||
startLegacy := time.Now()
|
startLegacy := time.Now()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, _, errObjectSt := d.Legacy.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
_, _, errObjectSt := d.Legacy.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||||
d.recordLegacyDuration(errObjectSt != nil, mode3Str, d.kind, method, startLegacy)
|
d.recordLegacyDuration(errObjectSt != nil, mode3Str, d.resource, method, startLegacy)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return res, async, err
|
return res, async, err
|
||||||
@@ -156,17 +162,17 @@ func (d *DualWriterMode3) DeleteCollection(ctx context.Context, deleteValidation
|
|||||||
res, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
res, err := d.Storage.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to delete collection in storage")
|
log.Error(err, "unable to delete collection in storage")
|
||||||
d.recordStorageDuration(true, mode3Str, d.kind, method, startStorage)
|
d.recordStorageDuration(true, mode3Str, d.resource, method, startStorage)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(false, mode3Str, d.kind, method, startStorage)
|
d.recordStorageDuration(false, mode3Str, d.resource, method, startStorage)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
startLegacy := time.Now()
|
startLegacy := time.Now()
|
||||||
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy deletecollection timeout"))
|
ctx, cancel := context.WithTimeoutCause(ctx, time.Second*10, errors.New("legacy deletecollection timeout"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
_, err := d.Legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
|
||||||
d.recordStorageDuration(err != nil, mode3Str, d.kind, method, startLegacy)
|
d.recordStorageDuration(err != nil, mode3Str, d.resource, method, startLegacy)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return res, err
|
return res, err
|
||||||
|
|||||||
@@ -17,16 +17,22 @@ type DualWriterMode4 struct {
|
|||||||
Storage Storage
|
Storage Storage
|
||||||
watchImp rest.Watcher // watch is only available in mode 3 and 4
|
watchImp rest.Watcher // watch is only available in mode 3 and 4
|
||||||
*dualWriterMetrics
|
*dualWriterMetrics
|
||||||
kind string
|
resource string
|
||||||
Log klog.Logger
|
Log klog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
const mode4Str = "4"
|
const mode4Str = "4"
|
||||||
|
|
||||||
// newDualWriterMode4 returns a new DualWriter in mode 4.
|
// newDualWriterMode4 returns a new DualWriter in mode 4.
|
||||||
// Mode 4 represents writing and reading from Storage.
|
// Mode 4 represents writing and reading from Storage.
|
||||||
func newDualWriterMode4(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, kind string) *DualWriterMode4 {
|
func newDualWriterMode4(legacy LegacyStorage, storage Storage, dwm *dualWriterMetrics, resource string) *DualWriterMode4 {
|
||||||
return &DualWriterMode4{Legacy: legacy, Storage: storage, Log: klog.NewKlogr().WithName("DualWriterMode4").WithValues("mode", mode4Str, "kind", kind), dualWriterMetrics: dwm}
|
return &DualWriterMode4{
|
||||||
|
Legacy: legacy,
|
||||||
|
Storage: storage,
|
||||||
|
Log: klog.NewKlogr().WithName("DualWriterMode4").WithValues("mode", mode4Str, "resource", resource),
|
||||||
|
dualWriterMetrics: dwm,
|
||||||
|
resource: resource,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mode returns the mode of the dual writer.
|
// Mode returns the mode of the dual writer.
|
||||||
@@ -47,7 +53,7 @@ func (d *DualWriterMode4) Create(ctx context.Context, obj runtime.Object, create
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to create object in storage")
|
log.Error(err, "unable to create object in storage")
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(err != nil, mode4Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode4Str, d.resource, method, startStorage)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +68,7 @@ func (d *DualWriterMode4) Get(ctx context.Context, name string, options *metav1.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to create object in storage")
|
log.Error(err, "unable to create object in storage")
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(err != nil, mode4Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode4Str, d.resource, method, startStorage)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +82,7 @@ func (d *DualWriterMode4) Delete(ctx context.Context, name string, deleteValidat
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to delete object in storage")
|
log.Error(err, "unable to delete object in storage")
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(err != nil, mode4Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode4Str, d.resource, method, startStorage)
|
||||||
return res, async, err
|
return res, async, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,14 +97,14 @@ func (d *DualWriterMode4) DeleteCollection(ctx context.Context, deleteValidation
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to delete collection in storage")
|
log.Error(err, "unable to delete collection in storage")
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(err != nil, mode4Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode4Str, d.resource, method, startStorage)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update overrides the generic behavior of the Storage and writes only to US.
|
// Update overrides the generic behavior of the Storage and writes only to US.
|
||||||
func (d *DualWriterMode4) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
func (d *DualWriterMode4) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||||
var method = "update"
|
var method = "update"
|
||||||
log := d.Log.WithValues("name", name, "kind", d.kind, "method", method)
|
log := d.Log.WithValues("name", name, "resource", d.resource, "method", method)
|
||||||
ctx = klog.NewContext(ctx, log)
|
ctx = klog.NewContext(ctx, log)
|
||||||
|
|
||||||
startStorage := time.Now()
|
startStorage := time.Now()
|
||||||
@@ -106,7 +112,7 @@ func (d *DualWriterMode4) Update(ctx context.Context, name string, objInfo rest.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to update object in storage")
|
log.Error(err, "unable to update object in storage")
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(err != nil, mode4Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode4Str, d.resource, method, startStorage)
|
||||||
return res, async, err
|
return res, async, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +126,7 @@ func (d *DualWriterMode4) List(ctx context.Context, options *metainternalversion
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err, "unable to list objects in storage")
|
log.Error(err, "unable to list objects in storage")
|
||||||
}
|
}
|
||||||
d.recordStorageDuration(err != nil, mode4Str, d.kind, method, startStorage)
|
d.recordStorageDuration(err != nil, mode4Str, d.resource, method, startStorage)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ var DualWriterStorageDuration = prometheus.NewHistogramVec(prometheus.HistogramO
|
|||||||
Help: "Histogram for the runtime of dual writer storage duration per mode",
|
Help: "Histogram for the runtime of dual writer storage duration per mode",
|
||||||
Namespace: "grafana",
|
Namespace: "grafana",
|
||||||
NativeHistogramBucketFactor: 1.1,
|
NativeHistogramBucketFactor: 1.1,
|
||||||
}, []string{"is_error", "mode", "kind", "method"})
|
}, []string{"is_error", "mode", "resource", "method"})
|
||||||
|
|
||||||
// DualWriterLegacyDuration is a metric summary for dual writer legacy duration per mode
|
// DualWriterLegacyDuration is a metric summary for dual writer legacy duration per mode
|
||||||
var DualWriterLegacyDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
var DualWriterLegacyDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
@@ -30,7 +30,7 @@ var DualWriterLegacyDuration = prometheus.NewHistogramVec(prometheus.HistogramOp
|
|||||||
Help: "Histogram for the runtime of dual writer legacy duration per mode",
|
Help: "Histogram for the runtime of dual writer legacy duration per mode",
|
||||||
Namespace: "grafana",
|
Namespace: "grafana",
|
||||||
NativeHistogramBucketFactor: 1.1,
|
NativeHistogramBucketFactor: 1.1,
|
||||||
}, []string{"is_error", "mode", "kind", "method"})
|
}, []string{"is_error", "mode", "resource", "method"})
|
||||||
|
|
||||||
// DualWriterOutcome is a metric summary for dual writer outcome comparison between the 2 stores per mode
|
// DualWriterOutcome is a metric summary for dual writer outcome comparison between the 2 stores per mode
|
||||||
var DualWriterOutcome = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
var DualWriterOutcome = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
@@ -44,7 +44,7 @@ var DualWriterReadLegacyCounts = prometheus.NewCounterVec(prometheus.CounterOpts
|
|||||||
Name: "dual_writer_read_legacy_count",
|
Name: "dual_writer_read_legacy_count",
|
||||||
Help: "Histogram for the runtime of dual writer reads from legacy",
|
Help: "Histogram for the runtime of dual writer reads from legacy",
|
||||||
Namespace: "grafana",
|
Namespace: "grafana",
|
||||||
}, []string{"kind", "method"})
|
}, []string{"resource", "method"})
|
||||||
|
|
||||||
// DualWriterSyncerDuration is a metric summary for dual writer sync duration per mode
|
// DualWriterSyncerDuration is a metric summary for dual writer sync duration per mode
|
||||||
var DualWriterSyncerDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
var DualWriterSyncerDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
@@ -52,7 +52,7 @@ var DualWriterSyncerDuration = prometheus.NewHistogramVec(prometheus.HistogramOp
|
|||||||
Help: "Histogram for the runtime of dual writer data syncer duration per mode",
|
Help: "Histogram for the runtime of dual writer data syncer duration per mode",
|
||||||
Namespace: "grafana",
|
Namespace: "grafana",
|
||||||
NativeHistogramBucketFactor: 1.1,
|
NativeHistogramBucketFactor: 1.1,
|
||||||
}, []string{"is_error", "mode", "kind"})
|
}, []string{"is_error", "mode", "resource"})
|
||||||
|
|
||||||
// DualWriterDataSyncerOutcome is a metric summary for dual writer data syncer outcome comparison between the 2 stores per mode
|
// DualWriterDataSyncerOutcome is a metric summary for dual writer data syncer outcome comparison between the 2 stores per mode
|
||||||
var DualWriterDataSyncerOutcome = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
var DualWriterDataSyncerOutcome = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||||
@@ -60,7 +60,7 @@ var DualWriterDataSyncerOutcome = prometheus.NewHistogramVec(prometheus.Histogra
|
|||||||
Help: "Histogram for the runtime of dual writer data syncer outcome comparison between the 2 stores per mode",
|
Help: "Histogram for the runtime of dual writer data syncer outcome comparison between the 2 stores per mode",
|
||||||
Namespace: "grafana",
|
Namespace: "grafana",
|
||||||
NativeHistogramBucketFactor: 1.1,
|
NativeHistogramBucketFactor: 1.1,
|
||||||
}, []string{"mode", "kind"})
|
}, []string{"mode", "resource"})
|
||||||
|
|
||||||
func (m *dualWriterMetrics) init(reg prometheus.Registerer) {
|
func (m *dualWriterMetrics) init(reg prometheus.Registerer) {
|
||||||
log := klog.NewKlogr()
|
log := klog.NewKlogr()
|
||||||
@@ -79,14 +79,14 @@ func (m *dualWriterMetrics) init(reg prometheus.Registerer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *dualWriterMetrics) recordLegacyDuration(isError bool, mode string, kind string, method string, startFrom time.Time) {
|
func (m *dualWriterMetrics) recordLegacyDuration(isError bool, mode string, resource string, method string, startFrom time.Time) {
|
||||||
duration := time.Since(startFrom).Seconds()
|
duration := time.Since(startFrom).Seconds()
|
||||||
m.legacy.WithLabelValues(strconv.FormatBool(isError), mode, kind, method).Observe(duration)
|
m.legacy.WithLabelValues(strconv.FormatBool(isError), mode, resource, method).Observe(duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *dualWriterMetrics) recordStorageDuration(isError bool, mode string, kind string, method string, startFrom time.Time) {
|
func (m *dualWriterMetrics) recordStorageDuration(isError bool, mode string, resource string, method string, startFrom time.Time) {
|
||||||
duration := time.Since(startFrom).Seconds()
|
duration := time.Since(startFrom).Seconds()
|
||||||
m.storage.WithLabelValues(strconv.FormatBool(isError), mode, kind, method).Observe(duration)
|
m.storage.WithLabelValues(strconv.FormatBool(isError), mode, resource, method).Observe(duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *dualWriterMetrics) recordOutcome(mode string, name string, areEqual bool, method string) {
|
func (m *dualWriterMetrics) recordOutcome(mode string, name string, areEqual bool, method string) {
|
||||||
@@ -97,15 +97,15 @@ func (m *dualWriterMetrics) recordOutcome(mode string, name string, areEqual boo
|
|||||||
m.outcome.WithLabelValues(mode, name, method).Observe(observeValue)
|
m.outcome.WithLabelValues(mode, name, method).Observe(observeValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *dualWriterMetrics) recordDataSyncerDuration(isError bool, mode string, kind string, startFrom time.Time) {
|
func (m *dualWriterMetrics) recordDataSyncerDuration(isError bool, mode string, resource string, startFrom time.Time) {
|
||||||
duration := time.Since(startFrom).Seconds()
|
duration := time.Since(startFrom).Seconds()
|
||||||
m.syncer.WithLabelValues(strconv.FormatBool(isError), mode, kind).Observe(duration)
|
m.syncer.WithLabelValues(strconv.FormatBool(isError), mode, resource).Observe(duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *dualWriterMetrics) recordDataSyncerOutcome(mode string, kind string, synced bool) {
|
func (m *dualWriterMetrics) recordDataSyncerOutcome(mode string, resource string, synced bool) {
|
||||||
var observeValue float64
|
var observeValue float64
|
||||||
if !synced {
|
if !synced {
|
||||||
observeValue = 1
|
observeValue = 1
|
||||||
}
|
}
|
||||||
m.syncerOutcome.WithLabelValues(mode, kind).Observe(observeValue)
|
m.syncerOutcome.WithLabelValues(mode, resource).Observe(observeValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ func doInstallPlugin(ctx context.Context, pluginID, version string, o pluginInst
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
ctx = repo.WithRequestOrigin(ctx, "cli")
|
||||||
archiveInfo, err := repository.GetPluginArchiveInfo(ctx, pluginID, version, compatOpts)
|
archiveInfo, err := repository.GetPluginArchiveInfo(ctx, pluginID, version, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CloseCallbackFunc func(bytesRead int64)
|
|
||||||
|
|
||||||
// CountBytesReader counts the total amount of bytes read from the underlying reader.
|
|
||||||
//
|
|
||||||
// The provided callback func will be called before the underlying reader is closed.
|
|
||||||
func CountBytesReader(reader io.ReadCloser, callback CloseCallbackFunc) io.ReadCloser {
|
|
||||||
if reader == nil {
|
|
||||||
panic("reader cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if callback == nil {
|
|
||||||
panic("callback cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &countBytesReader{reader: reader, callback: callback}
|
|
||||||
}
|
|
||||||
|
|
||||||
type countBytesReader struct {
|
|
||||||
reader io.ReadCloser
|
|
||||||
callback CloseCallbackFunc
|
|
||||||
counter int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *countBytesReader) Read(p []byte) (int, error) {
|
|
||||||
n, err := r.reader.Read(p)
|
|
||||||
r.counter += int64(n)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *countBytesReader) Close() error {
|
|
||||||
r.callback(r.counter)
|
|
||||||
return r.reader.Close()
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCountBytesReader(t *testing.T) {
|
|
||||||
tcs := []struct {
|
|
||||||
body string
|
|
||||||
expectedBytesCount int64
|
|
||||||
}{
|
|
||||||
{body: "d", expectedBytesCount: 1},
|
|
||||||
{body: "dummy", expectedBytesCount: 5},
|
|
||||||
}
|
|
||||||
|
|
||||||
for index, tc := range tcs {
|
|
||||||
t.Run(fmt.Sprintf("Test CountBytesReader %d", index), func(t *testing.T) {
|
|
||||||
body := io.NopCloser(strings.NewReader(tc.body))
|
|
||||||
var actualBytesRead int64
|
|
||||||
|
|
||||||
readCloser := CountBytesReader(body, func(bytesRead int64) {
|
|
||||||
actualBytesRead = bytesRead
|
|
||||||
})
|
|
||||||
|
|
||||||
bodyBytes, err := io.ReadAll(readCloser)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = readCloser.Close()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.expectedBytesCount, actualBytesRead)
|
|
||||||
require.Equal(t, string(bodyBytes), tc.body)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
@@ -113,7 +112,7 @@ func executeMiddleware(next http.RoundTripper, labels prometheus.Labels) http.Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res != nil && res.StatusCode != http.StatusSwitchingProtocols {
|
if res != nil && res.StatusCode != http.StatusSwitchingProtocols {
|
||||||
res.Body = httpclient.CountBytesReader(res.Body, func(bytesRead int64) {
|
res.Body = sdkhttpclient.CountBytesReader(res.Body, func(bytesRead int64) {
|
||||||
responseSizeHistogram.Observe(float64(bytesRead))
|
responseSizeHistogram.Observe(float64(bytesRead))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ type FakePluginRepo struct {
|
|||||||
GetPluginArchiveFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error)
|
GetPluginArchiveFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchive, error)
|
||||||
GetPluginArchiveByURLFunc func(_ context.Context, archiveURL string, _ repo.CompatOpts) (*repo.PluginArchive, error)
|
GetPluginArchiveByURLFunc func(_ context.Context, archiveURL string, _ repo.CompatOpts) (*repo.PluginArchive, error)
|
||||||
GetPluginArchiveInfoFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error)
|
GetPluginArchiveInfoFunc func(_ context.Context, pluginID, version string, _ repo.CompatOpts) (*repo.PluginArchiveInfo, error)
|
||||||
PluginVersionFunc func(pluginID, version string, compatOpts repo.CompatOpts) (repo.VersionData, error)
|
PluginVersionFunc func(_ context.Context, pluginID, version string, compatOpts repo.CompatOpts) (repo.VersionData, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPluginArchive fetches the requested plugin archive.
|
// GetPluginArchive fetches the requested plugin archive.
|
||||||
@@ -260,9 +260,9 @@ func (r *FakePluginRepo) GetPluginArchiveInfo(ctx context.Context, pluginID, ver
|
|||||||
return &repo.PluginArchiveInfo{}, nil
|
return &repo.PluginArchiveInfo{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FakePluginRepo) PluginVersion(pluginID, version string, compatOpts repo.CompatOpts) (repo.VersionData, error) {
|
func (r *FakePluginRepo) PluginVersion(ctx context.Context, pluginID, version string, compatOpts repo.CompatOpts) (repo.VersionData, error) {
|
||||||
if r.PluginVersionFunc != nil {
|
if r.PluginVersionFunc != nil {
|
||||||
return r.PluginVersionFunc(pluginID, version, compatOpts)
|
return r.PluginVersionFunc(ctx, pluginID, version, compatOpts)
|
||||||
}
|
}
|
||||||
return repo.VersionData{}, nil
|
return repo.VersionData{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-13
@@ -35,7 +35,15 @@ func NewClient(skipTLSVerify bool, logger log.PrettyLogger) *Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Download(_ context.Context, pluginZipURL, checksum string, compatOpts CompatOpts) (*PluginArchive, error) {
|
type requestOrigin struct{}
|
||||||
|
|
||||||
|
// WithRequestOrigin adds the request origin to the context which is used
|
||||||
|
// to set the `grafana-origin` header in the outgoing HTTP request.
|
||||||
|
func WithRequestOrigin(ctx context.Context, origin string) context.Context {
|
||||||
|
return context.WithValue(ctx, requestOrigin{}, origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Download(ctx context.Context, pluginZipURL, checksum string, compatOpts CompatOpts) (*PluginArchive, error) {
|
||||||
// Create temp file for downloading zip file
|
// Create temp file for downloading zip file
|
||||||
tmpFile, err := os.CreateTemp("", "*.zip")
|
tmpFile, err := os.CreateTemp("", "*.zip")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -49,7 +57,7 @@ func (c *Client) Download(_ context.Context, pluginZipURL, checksum string, comp
|
|||||||
|
|
||||||
c.log.Debugf("Installing plugin from %s", pluginZipURL)
|
c.log.Debugf("Installing plugin from %s", pluginZipURL)
|
||||||
|
|
||||||
err = c.downloadFile(tmpFile, pluginZipURL, checksum, compatOpts)
|
err = c.downloadFile(ctx, tmpFile, pluginZipURL, checksum, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err := tmpFile.Close(); err != nil {
|
if err := tmpFile.Close(); err != nil {
|
||||||
c.log.Warn("Failed to close file", "error", err)
|
c.log.Warn("Failed to close file", "error", err)
|
||||||
@@ -65,8 +73,8 @@ func (c *Client) Download(_ context.Context, pluginZipURL, checksum string, comp
|
|||||||
return &PluginArchive{File: rc}, nil
|
return &PluginArchive{File: rc}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SendReq(url *url.URL, compatOpts CompatOpts) ([]byte, error) {
|
func (c *Client) SendReq(ctx context.Context, url *url.URL, compatOpts CompatOpts) ([]byte, error) {
|
||||||
req, err := c.createReq(url, compatOpts)
|
req, err := c.createReq(ctx, url, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -87,7 +95,7 @@ func (c *Client) SendReq(url *url.URL, compatOpts CompatOpts) ([]byte, error) {
|
|||||||
return io.ReadAll(bodyReader)
|
return io.ReadAll(bodyReader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, compatOpts CompatOpts) (err error) {
|
func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, checksum string, compatOpts CompatOpts) (err error) {
|
||||||
// Try handling URL as a local file path first
|
// Try handling URL as a local file path first
|
||||||
if _, err := os.Stat(pluginURL); err == nil {
|
if _, err := os.Stat(pluginURL); err == nil {
|
||||||
// TODO re-verify
|
// TODO re-verify
|
||||||
@@ -110,13 +118,11 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c.retryCount = 0
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
c.retryCount++
|
c.retryCount++
|
||||||
if c.retryCount < 3 {
|
if c.retryCount < 3 {
|
||||||
c.log.Debug("Failed downloading. Will retry once.")
|
c.log.Debug("Failed downloading. Will retry.")
|
||||||
err = tmpFile.Truncate(0)
|
err = tmpFile.Truncate(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -125,7 +131,7 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = c.downloadFile(tmpFile, pluginURL, checksum, compatOpts)
|
err = c.downloadFile(ctx, tmpFile, pluginURL, checksum, compatOpts)
|
||||||
} else {
|
} else {
|
||||||
c.retryCount = 0
|
c.retryCount = 0
|
||||||
failure := fmt.Sprintf("%v", r)
|
failure := fmt.Sprintf("%v", r)
|
||||||
@@ -145,8 +151,13 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
|
|||||||
|
|
||||||
// Using no timeout as some plugin archives make take longer to fetch due to size, network performance, etc.
|
// Using no timeout as some plugin archives make take longer to fetch due to size, network performance, etc.
|
||||||
// Note: This is also used as part of the grafana plugin install CLI operation
|
// Note: This is also used as part of the grafana plugin install CLI operation
|
||||||
bodyReader, err := c.sendReqNoTimeout(u, compatOpts)
|
bodyReader, err := c.sendReqNoTimeout(ctx, u, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if c.retryCount < 3 {
|
||||||
|
c.retryCount++
|
||||||
|
c.log.Debug("Failed downloading. Will retry.")
|
||||||
|
err = c.downloadFile(ctx, tmpFile, pluginURL, checksum, compatOpts)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -166,11 +177,14 @@ func (c *Client) downloadFile(tmpFile *os.File, pluginURL, checksum string, comp
|
|||||||
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
|
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
|
||||||
return ErrChecksumMismatch(pluginURL)
|
return ErrChecksumMismatch(pluginURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.retryCount = 0
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) sendReqNoTimeout(url *url.URL, compatOpts CompatOpts) (io.ReadCloser, error) {
|
func (c *Client) sendReqNoTimeout(ctx context.Context, url *url.URL, compatOpts CompatOpts) (io.ReadCloser, error) {
|
||||||
req, err := c.createReq(url, compatOpts)
|
req, err := c.createReq(ctx, url, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -182,7 +196,7 @@ func (c *Client) sendReqNoTimeout(url *url.URL, compatOpts CompatOpts) (io.ReadC
|
|||||||
return c.handleResp(res, compatOpts)
|
return c.handleResp(res, compatOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) createReq(url *url.URL, compatOpts CompatOpts) (*http.Request, error) {
|
func (c *Client) createReq(ctx context.Context, url *url.URL, compatOpts CompatOpts) (*http.Request, error) {
|
||||||
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -201,6 +215,14 @@ func (c *Client) createReq(url *url.URL, compatOpts CompatOpts) (*http.Request,
|
|||||||
req.Header.Set("grafana-arch", sysArch)
|
req.Header.Set("grafana-arch", sysArch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.retryCount > 0 {
|
||||||
|
req.Header.Set("grafana-retrycount", fmt.Sprintf("%d", c.retryCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
if orig := ctx.Value(requestOrigin{}); orig != nil {
|
||||||
|
req.Header.Set("grafana-origin", orig.(string))
|
||||||
|
}
|
||||||
|
|
||||||
return req, err
|
return req, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFakeZip(w http.ResponseWriter) error {
|
||||||
|
ww := zip.NewWriter(w)
|
||||||
|
_, err := ww.Create("test.txt")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ww.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_Download(t *testing.T) {
|
||||||
|
t.Run("it should download a file", func(t *testing.T) {
|
||||||
|
fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := writeFakeZip(w)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}))
|
||||||
|
defer fakeServer.Close()
|
||||||
|
cli := fakeServer.Client()
|
||||||
|
repo := Client{httpClient: *cli, httpClientNoTimeout: *cli, log: log.NewPrettyLogger("test")}
|
||||||
|
_, err := repo.Download(context.Background(), fakeServer.URL, "", CompatOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("it should set the origin header", func(t *testing.T) {
|
||||||
|
var origin string
|
||||||
|
fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
origin = r.Header.Get("grafana-origin")
|
||||||
|
err := writeFakeZip(w)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}))
|
||||||
|
defer fakeServer.Close()
|
||||||
|
cli := fakeServer.Client()
|
||||||
|
repo := Client{httpClient: *cli, httpClientNoTimeout: *cli, log: log.NewPrettyLogger("test")}
|
||||||
|
ctx := WithRequestOrigin(context.Background(), "test")
|
||||||
|
_, err := repo.Download(ctx, fakeServer.URL, "", CompatOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "test", origin, "origin header should be set")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("it should retry on error", func(t *testing.T) {
|
||||||
|
var count int
|
||||||
|
fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
count++
|
||||||
|
if count < 2 {
|
||||||
|
http.Error(w, "error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
retryCount := r.Header.Get("grafana-retrycount")
|
||||||
|
require.Equal(t, "2", retryCount, "retry count should be set")
|
||||||
|
err := writeFakeZip(w)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}))
|
||||||
|
defer fakeServer.Close()
|
||||||
|
cli := fakeServer.Client()
|
||||||
|
repo := Client{httpClient: *cli, httpClientNoTimeout: *cli, log: log.NewPrettyLogger("test"), retryCount: 1}
|
||||||
|
_, err := repo.Download(context.Background(), fakeServer.URL, "", CompatOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, count, "should retry on error")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ type Service interface {
|
|||||||
// GetPluginArchiveInfo fetches information needed for downloading the requested plugin.
|
// GetPluginArchiveInfo fetches information needed for downloading the requested plugin.
|
||||||
GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchiveInfo, error)
|
GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchiveInfo, error)
|
||||||
// PluginVersion will return plugin version based on the requested information.
|
// PluginVersion will return plugin version based on the requested information.
|
||||||
PluginVersion(pluginID, version string, compatOpts CompatOpts) (VersionData, error)
|
PluginVersion(ctx context.Context, pluginID, version string, compatOpts CompatOpts) (VersionData, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompatOpts struct {
|
type CompatOpts struct {
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ func (m *Manager) GetPluginArchiveByURL(ctx context.Context, pluginZipURL string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetPluginArchiveInfo returns the options for downloading the requested plugin (with optional `version`)
|
// GetPluginArchiveInfo returns the options for downloading the requested plugin (with optional `version`)
|
||||||
func (m *Manager) GetPluginArchiveInfo(_ context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginArchiveInfo, error) {
|
func (m *Manager) GetPluginArchiveInfo(ctx context.Context, pluginID, version string, compatOpts CompatOpts) (*PluginArchiveInfo, error) {
|
||||||
v, err := m.PluginVersion(pluginID, version, compatOpts)
|
v, err := m.PluginVersion(ctx, pluginID, version, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -77,8 +77,8 @@ func (m *Manager) GetPluginArchiveInfo(_ context.Context, pluginID, version stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PluginVersion will return plugin version based on the requested information
|
// PluginVersion will return plugin version based on the requested information
|
||||||
func (m *Manager) PluginVersion(pluginID, version string, compatOpts CompatOpts) (VersionData, error) {
|
func (m *Manager) PluginVersion(ctx context.Context, pluginID, version string, compatOpts CompatOpts) (VersionData, error) {
|
||||||
versions, err := m.grafanaCompatiblePluginVersions(pluginID, compatOpts)
|
versions, err := m.grafanaCompatiblePluginVersions(ctx, pluginID, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return VersionData{}, err
|
return VersionData{}, err
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func (m *Manager) downloadURL(pluginID, version string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// grafanaCompatiblePluginVersions will get version info from /api/plugins/$pluginID/versions
|
// grafanaCompatiblePluginVersions will get version info from /api/plugins/$pluginID/versions
|
||||||
func (m *Manager) grafanaCompatiblePluginVersions(pluginID string, compatOpts CompatOpts) ([]Version, error) {
|
func (m *Manager) grafanaCompatiblePluginVersions(ctx context.Context, pluginID string, compatOpts CompatOpts) ([]Version, error) {
|
||||||
u, err := url.Parse(m.baseURL)
|
u, err := url.Parse(m.baseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -111,7 +111,7 @@ func (m *Manager) grafanaCompatiblePluginVersions(pluginID string, compatOpts Co
|
|||||||
|
|
||||||
u.Path = path.Join(u.Path, pluginID, "versions")
|
u.Path = path.Join(u.Path, pluginID, "versions")
|
||||||
|
|
||||||
body, err := m.client.SendReq(u, compatOpts)
|
body, err := m.client.SendReq(ctx, u, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ go 1.23.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3
|
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/prometheus/client_golang v1.20.0
|
github.com/prometheus/client_golang v1.20.0
|
||||||
|
|||||||
+2
-2
@@ -98,8 +98,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 h1:as4PmrFoYI1byS5JjsgPC7uSGTMh+SgS0ePv6hOyDGU=
|
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 h1:as4PmrFoYI1byS5JjsgPC7uSGTMh+SgS0ePv6hOyDGU=
|
||||||
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3/go.mod h1:lcjGB6SuaZ2o44A9nD6p/tR4QXSPbzViRY520Gy6pTQ=
|
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3/go.mod h1:lcjGB6SuaZ2o44A9nD6p/tR4QXSPbzViRY520Gy6pTQ=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0 h1:ZZxHbiiF6QcsnlbPFyZGmzNDoTC1pLeHXUQYoskWt5c=
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0 h1:2KCKA86//O20ffL6WKzHGx5scBbdV7GyEFGnH8Hdv7M=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.244.0/go.mod h1:H3FXrJMUlwocQ6UYj8Ds5I9EzRAVOcdRcgaRE3mXQqk=
|
github.com/grafana/grafana-plugin-sdk-go v0.245.0/go.mod h1:1X8Kgo/SK91Qo1WBCKjPSKrfgjpQys1OkQsHhA78TLg=
|
||||||
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||||
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ func New(
|
|||||||
|
|
||||||
func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
fromAlert := req.Headers["FromAlert"] == "true"
|
fromAlert := req.Headers["FromAlert"] == "true"
|
||||||
|
logger := s.log.FromContext(ctx)
|
||||||
|
logger.Debug("Begin query execution", "fromAlert", fromAlert)
|
||||||
result := backend.QueryDataResponse{
|
result := backend.QueryDataResponse{
|
||||||
Responses: backend.Responses{},
|
Responses: backend.Responses{},
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,6 @@ func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest)
|
|||||||
|
|
||||||
concurrentQueryCount, err := req.PluginContext.GrafanaConfig.ConcurrentQueryCount()
|
concurrentQueryCount, err := req.PluginContext.GrafanaConfig.ConcurrentQueryCount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger := s.log.FromContext(ctx)
|
|
||||||
logger.Debug(fmt.Sprintf("Concurrent Query Count read/parse error: %v", err), "prometheusRunQueriesInParallel")
|
logger.Debug(fmt.Sprintf("Concurrent Query Count read/parse error: %v", err), "prometheusRunQueriesInParallel")
|
||||||
concurrentQueryCount = 10
|
concurrentQueryCount = 10
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
@@ -45,6 +46,7 @@ type DataSourceAPIBuilder struct {
|
|||||||
contextProvider PluginContextWrapper
|
contextProvider PluginContextWrapper
|
||||||
accessControl accesscontrol.AccessControl
|
accessControl accesscontrol.AccessControl
|
||||||
queryTypes *query.QueryTypeDefinitionList
|
queryTypes *query.QueryTypeDefinitionList
|
||||||
|
log log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterAPIService(
|
func RegisterAPIService(
|
||||||
@@ -121,6 +123,7 @@ func NewDataSourceAPIBuilder(
|
|||||||
datasources: datasources,
|
datasources: datasources,
|
||||||
contextProvider: contextProvider,
|
contextProvider: contextProvider,
|
||||||
accessControl: accessControl,
|
accessControl: accessControl,
|
||||||
|
log: log.New("grafana-apiserver.datasource"),
|
||||||
}
|
}
|
||||||
if loadQueryTypes {
|
if loadQueryTypes {
|
||||||
// In the future, this will somehow come from the plugin
|
// In the future, this will somehow come from the plugin
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||||
@@ -52,7 +53,6 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
dqr := data.QueryDataRequest{}
|
dqr := data.QueryDataRequest{}
|
||||||
err := web.Bind(req, &dqr)
|
err := web.Bind(req, &dqr)
|
||||||
@@ -73,9 +73,30 @@ func (r *subQueryREST) Connect(ctx context.Context, name string, opts runtime.Ob
|
|||||||
|
|
||||||
ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig)
|
ctx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig)
|
||||||
ctx = contextualMiddlewares(ctx)
|
ctx = contextualMiddlewares(ctx)
|
||||||
|
|
||||||
|
// only forward expected headers, log unexpected ones
|
||||||
|
headers := make(map[string]string)
|
||||||
|
// headers are case insensitive, however some datasources still check for camel casing so we have to send them camel cased
|
||||||
|
expectedHeaders := map[string]string{
|
||||||
|
"fromalert": "FromAlert",
|
||||||
|
"content-type": "Content-Type",
|
||||||
|
"content-length": "Content-Length",
|
||||||
|
"user-agent": "User-Agent",
|
||||||
|
"accept": "Accept",
|
||||||
|
}
|
||||||
|
for k, v := range req.Header {
|
||||||
|
headerToSend, ok := expectedHeaders[strings.ToLower(k)]
|
||||||
|
if ok {
|
||||||
|
headers[headerToSend] = v[0]
|
||||||
|
} else {
|
||||||
|
r.builder.log.Warn("datasource received an unexpected header, ignoring it", "header", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rsp, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{
|
rsp, err := r.builder.client.QueryData(ctx, &backend.QueryDataRequest{
|
||||||
Queries: queries,
|
Queries: queries,
|
||||||
PluginContext: pluginCtx,
|
PluginContext: pluginCtx,
|
||||||
|
Headers: headers,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
responder.Error(err)
|
responder.Error(err)
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package datasource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubQueryConnect(t *testing.T) {
|
||||||
|
sqr := subQueryREST{
|
||||||
|
builder: &DataSourceAPIBuilder{
|
||||||
|
client: mockClient{
|
||||||
|
lastCalledWithHeaders: &map[string]string{},
|
||||||
|
},
|
||||||
|
datasources: mockDatasources{},
|
||||||
|
contextProvider: mockContextProvider{},
|
||||||
|
log: log.NewNopLogger(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mr := mockResponder{}
|
||||||
|
handler, err := sqr.Connect(context.Background(), "dsname", nil, mr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/some-path", nil)
|
||||||
|
req.Header.Set("fromAlert", "true")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
// test that headers are forwarded and cased appropriately
|
||||||
|
require.Equal(t, map[string]string{
|
||||||
|
"FromAlert": "true",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}, *sqr.builder.client.(mockClient).lastCalledWithHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockClient struct {
|
||||||
|
lastCalledWithHeaders *map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
*m.lastCalledWithHeaders = req.Headers
|
||||||
|
return nil, fmt.Errorf("mock error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockResponder struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object writes the provided object to the response. Invoking this method multiple times is undefined.
|
||||||
|
func (m mockResponder) Object(statusCode int, obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error writes the provided error to the response. This method may only be invoked once.
|
||||||
|
func (m mockResponder) Error(err error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockDatasources struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a specific datasource (that the user in context can see)
|
||||||
|
func (m mockDatasources) Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List lists all data sources the user in context can see
|
||||||
|
func (m mockDatasources) List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return settings (decrypted!) for a specific plugin
|
||||||
|
// This will require "query" permission for the user in context
|
||||||
|
func (m mockDatasources) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockContextProvider struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockContextProvider) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) {
|
||||||
|
return backend.PluginContext{}, nil
|
||||||
|
}
|
||||||
@@ -9,14 +9,13 @@ import (
|
|||||||
// The query runner interface
|
// The query runner interface
|
||||||
type DataSourceClientSupplier interface {
|
type DataSourceClientSupplier interface {
|
||||||
// Get a client for a given datasource
|
// Get a client for a given datasource
|
||||||
// NOTE: authorization headers are not yet added and the client may be shared across multiple users
|
GetDataSourceClient(ctx context.Context, ref data.DataSourceRef, headers map[string]string) (data.QueryDataClient, error)
|
||||||
GetDataSourceClient(ctx context.Context, ref data.DataSourceRef) (data.QueryDataClient, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommonDataSourceClientSupplier struct {
|
type CommonDataSourceClientSupplier struct {
|
||||||
Client data.QueryDataClient
|
Client data.QueryDataClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CommonDataSourceClientSupplier) GetDataSourceClient(ctx context.Context, ref data.DataSourceRef) (data.QueryDataClient, error) {
|
func (s *CommonDataSourceClientSupplier) GetDataSourceClient(_ context.Context, _ data.DataSourceRef, _ map[string]string) (data.QueryDataClient, error) {
|
||||||
return s.Client, nil
|
return s.Client, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
@@ -48,7 +49,7 @@ func newQueryREST(builder *QueryAPIBuilder) *queryREST {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryREST) New() runtime.Object {
|
func (r *queryREST) New() runtime.Object {
|
||||||
// This is added as the "ResponseType" regarless what ProducesObject() says :)
|
// This is added as the "ResponseType" regardless what ProducesObject() says :)
|
||||||
return &query.QueryDataResponse{}
|
return &query.QueryDataResponse{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +135,28 @@ func (r *queryREST) Connect(connectCtx context.Context, name string, _ runtime.O
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get headers from the original http req and add them to each sub request
|
||||||
|
// headers are case insensitive, however some datasources still check for camel casing so we have to send them camel cased
|
||||||
|
expectedHeaders := map[string]string{
|
||||||
|
"fromalert": "FromAlert",
|
||||||
|
"content-type": "Content-Type",
|
||||||
|
"content-length": "Content-Length",
|
||||||
|
"user-agent": "User-Agent",
|
||||||
|
"accept": "Accept",
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range req.Requests {
|
||||||
|
req.Requests[i].Headers = make(map[string]string)
|
||||||
|
for k, v := range httpreq.Header {
|
||||||
|
headerToSend, ok := expectedHeaders[strings.ToLower(k)]
|
||||||
|
if ok {
|
||||||
|
req.Requests[i].Headers[headerToSend] = v[0]
|
||||||
|
} else {
|
||||||
|
b.log.Warn(fmt.Sprintf("query service received an unexpected header, ignoring it: %s", k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Actually run the query
|
// Actually run the query
|
||||||
rsp, err := b.execute(ctx, req)
|
rsp, err := b.execute(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -192,11 +215,14 @@ func (b *QueryAPIBuilder) handleQuerySingleDatasource(ctx context.Context, req d
|
|||||||
return &backend.QueryDataResponse{}, nil
|
return &backend.QueryDataResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user headers... here or in client.QueryData
|
client, err := b.client.GetDataSourceClient(
|
||||||
client, err := b.client.GetDataSourceClient(ctx, v0alpha1.DataSourceRef{
|
ctx,
|
||||||
Type: req.PluginId,
|
v0alpha1.DataSourceRef{
|
||||||
UID: req.UID,
|
Type: req.PluginId,
|
||||||
})
|
UID: req.UID,
|
||||||
|
},
|
||||||
|
req.Headers,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||||
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQueryRestConnectHandler(t *testing.T) {
|
||||||
|
b := &QueryAPIBuilder{
|
||||||
|
client: mockClient{
|
||||||
|
lastCalledWithHeaders: &map[string]string{},
|
||||||
|
},
|
||||||
|
tracer: tracing.InitializeTracerForTest(),
|
||||||
|
parser: newQueryParser(expr.NewExpressionQueryReader(featuremgmt.WithFeatures()),
|
||||||
|
&legacyDataSourceRetriever{}, tracing.InitializeTracerForTest()),
|
||||||
|
log: log.New("test"),
|
||||||
|
}
|
||||||
|
qr := newQueryREST(b)
|
||||||
|
ctx := context.Background()
|
||||||
|
mr := mockResponder{}
|
||||||
|
|
||||||
|
handler, err := qr.Connect(ctx, "name", nil, mr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
body := runtime.RawExtension{
|
||||||
|
Raw: []byte(`{
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "demo-prometheus"
|
||||||
|
},
|
||||||
|
"expr": "sum(go_gc_duration_seconds)",
|
||||||
|
"range": false,
|
||||||
|
"instant": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"}`),
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/some-path", bytes.NewReader(body.Raw))
|
||||||
|
req.Header.Set("fromAlert", "true")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("some-unexpected-header", "some-value")
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
require.Equal(t, map[string]string{
|
||||||
|
"FromAlert": "true",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}, *b.client.(mockClient).lastCalledWithHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockResponder struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object writes the provided object to the response. Invoking this method multiple times is undefined.
|
||||||
|
func (m mockResponder) Object(statusCode int, obj runtime.Object) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error writes the provided error to the response. This method may only be invoked once.
|
||||||
|
func (m mockResponder) Error(err error) {
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockClient struct {
|
||||||
|
lastCalledWithHeaders *map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockClient) GetDataSourceClient(ctx context.Context, ref data.DataSourceRef, headers map[string]string) (data.QueryDataClient, error) {
|
||||||
|
*m.lastCalledWithHeaders = headers
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("mock error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
return nil, fmt.Errorf("mock error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ func (r *findREST) Connect(ctx context.Context, name string, opts runtime.Object
|
|||||||
results := &scope.FindScopeNodeChildrenResults{}
|
results := &scope.FindScopeNodeChildrenResults{}
|
||||||
|
|
||||||
raw, err := r.scopeNodeStorage.List(ctx, &internalversion.ListOptions{
|
raw, err := r.scopeNodeStorage.List(ctx, &internalversion.ListOptions{
|
||||||
Limit: 1000,
|
Limit: 10000,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,6 +80,7 @@ func (r *findREST) Connect(ctx context.Context, name string, opts runtime.Object
|
|||||||
}
|
}
|
||||||
|
|
||||||
all, ok := raw.(*scope.ScopeNodeList)
|
all, ok := raw.(*scope.ScopeNodeList)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
responder.Error(fmt.Errorf("expected ScopeNodeList"))
|
responder.Error(fmt.Errorf("expected ScopeNodeList"))
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ func (f *findScopeDashboardsREST) Connect(ctx context.Context, name string, opts
|
|||||||
}
|
}
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
raw, err := f.scopeDashboardStorage.List(ctx, &internalversion.ListOptions{})
|
raw, err := f.scopeDashboardStorage.List(ctx, &internalversion.ListOptions{
|
||||||
|
Limit: 10000,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -133,11 +133,12 @@ func (b *ScopeAPIBuilder) GetAPIGroupInfo(
|
|||||||
}
|
}
|
||||||
storage[scopeResourceInfo.StoragePath()] = scopeStorage
|
storage[scopeResourceInfo.StoragePath()] = scopeStorage
|
||||||
|
|
||||||
scopeDashboardStorage, err := newScopeDashboardBindingStorage(scheme, optsGetter)
|
scopeDashboardStorage, scopedDashboardStatusStorage, err := newScopeDashboardBindingStorage(scheme, optsGetter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
storage[scopeDashboardResourceInfo.StoragePath()] = scopeDashboardStorage
|
storage[scopeDashboardResourceInfo.StoragePath()] = scopeDashboardStorage
|
||||||
|
storage[scopeDashboardResourceInfo.StoragePath()+"/status"] = scopedDashboardStatusStorage
|
||||||
|
|
||||||
scopeNodeStorage, err := newScopeNodeStorage(scheme, optsGetter)
|
scopeNodeStorage, err := newScopeNodeStorage(scheme, optsGetter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func newScopeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGette
|
|||||||
return &storage{Store: store}, nil
|
return &storage{Store: store}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScopeDashboardBindingStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) {
|
func newScopeDashboardBindingStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, *grafanaregistry.StatusREST, error) {
|
||||||
resourceInfo := scope.ScopeDashboardBindingResourceInfo
|
resourceInfo := scope.ScopeDashboardBindingResourceInfo
|
||||||
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
|
strategy := grafanaregistry.NewStrategy(scheme, resourceInfo.GroupVersion())
|
||||||
|
|
||||||
@@ -63,10 +63,12 @@ func newScopeDashboardBindingStorage(scheme *runtime.Scheme, optsGetter generic.
|
|||||||
}
|
}
|
||||||
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
|
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
|
||||||
if err := store.CompleteWithOptions(options); err != nil {
|
if err := store.CompleteWithOptions(options); err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &storage{Store: store}, nil
|
statusStrategy := grafanaregistry.NewStatusStrategy(scheme, resourceInfo.GroupVersion())
|
||||||
|
statusREST := grafanaregistry.NewStatusREST(store, statusStrategy)
|
||||||
|
return &storage{Store: store}, statusREST, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScopeNodeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) {
|
func newScopeNodeStorage(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*storage, error) {
|
||||||
|
|||||||
@@ -406,7 +406,9 @@ func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistrat
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range r.Role.Permissions {
|
for i := range r.Role.Permissions {
|
||||||
s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope)
|
if err := s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.registrations.Append(r)
|
s.registrations.Append(r)
|
||||||
@@ -464,7 +466,9 @@ func (s *Service) DeclarePluginRoles(ctx context.Context, ID, name string, regs
|
|||||||
for i := range r.Role.Permissions {
|
for i := range r.Role.Permissions {
|
||||||
// Register plugin actions and their possible scopes for permission validation
|
// Register plugin actions and their possible scopes for permission validation
|
||||||
s.permRegistry.RegisterPluginScope(r.Role.Permissions[i].Scope)
|
s.permRegistry.RegisterPluginScope(r.Role.Permissions[i].Scope)
|
||||||
s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope)
|
if err := s.permRegistry.RegisterPermission(r.Role.Permissions[i].Action, r.Role.Permissions[i].Scope); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.Debug("Registering plugin role", "role", r.Role.Name)
|
s.log.Debug("Registering plugin role", "role", r.Role.Name)
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ func (api *AccessControlAPI) getUserActions(c *contextmodel.ReqContext) response
|
|||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
reloadCache := c.QueryBool("reloadcache")
|
reloadCache := c.QueryBool("reloadcache")
|
||||||
permissions, err := api.Service.GetUserPermissions(ctx,
|
permissions, err := api.Service.GetUserPermissions(ctx, c.SignedInUser, ac.Options{ReloadCache: reloadCache})
|
||||||
c.SignedInUser, ac.Options{ReloadCache: reloadCache})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.JSON(http.StatusInternalServerError, err)
|
return response.JSON(http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
@@ -65,8 +64,7 @@ func (api *AccessControlAPI) getUserPermissions(c *contextmodel.ReqContext) resp
|
|||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
reloadCache := c.QueryBool("reloadcache")
|
reloadCache := c.QueryBool("reloadcache")
|
||||||
permissions, err := api.Service.GetUserPermissions(ctx,
|
permissions, err := api.Service.GetUserPermissions(ctx, c.SignedInUser, ac.Options{ReloadCache: reloadCache})
|
||||||
c.SignedInUser, ac.Options{ReloadCache: reloadCache})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.JSON(http.StatusInternalServerError, err)
|
return response.JSON(http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ var (
|
|||||||
|
|
||||||
ErrUnknownActionTplt = "unknown action: {{.Public.Action}}, was not found in the list of valid actions"
|
ErrUnknownActionTplt = "unknown action: {{.Public.Action}}, was not found in the list of valid actions"
|
||||||
ErrBaseUnknownAction = errutil.BadRequest("permreg.unknown-action").MustTemplate(ErrUnknownActionTplt, errutil.WithPublic(ErrUnknownActionTplt))
|
ErrBaseUnknownAction = errutil.BadRequest("permreg.unknown-action").MustTemplate(ErrUnknownActionTplt, errutil.WithPublic(ErrUnknownActionTplt))
|
||||||
|
|
||||||
|
ErrBaseUnknownKind = errutil.BadRequest("permreg.unknown-kind").MustTemplate("unknown kind: {{.Public.Kind}}")
|
||||||
)
|
)
|
||||||
|
|
||||||
func ErrInvalidScope(scope string, action string, validScopePrefixes PrefixSet) error {
|
func ErrInvalidScope(scope string, action string, validScopePrefixes PrefixSet) error {
|
||||||
@@ -28,6 +30,10 @@ func ErrUnknownAction(action string) error {
|
|||||||
return ErrBaseUnknownAction.Build(errutil.TemplateData{Public: map[string]any{"Action": action}})
|
return ErrBaseUnknownAction.Build(errutil.TemplateData{Public: map[string]any{"Action": action}})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrUnknownKind(kind string) error {
|
||||||
|
return ErrBaseUnknownKind.Build(errutil.TemplateData{Public: map[string]any{"Kind": kind}})
|
||||||
|
}
|
||||||
|
|
||||||
func generateValidScopeFormats(acceptedScopePrefixes PrefixSet) []string {
|
func generateValidScopeFormats(acceptedScopePrefixes PrefixSet) []string {
|
||||||
if len(acceptedScopePrefixes) == 0 {
|
if len(acceptedScopePrefixes) == 0 {
|
||||||
return []string{}
|
return []string{}
|
||||||
@@ -48,7 +54,7 @@ func generateValidScopeFormats(acceptedScopePrefixes PrefixSet) []string {
|
|||||||
|
|
||||||
type PermissionRegistry interface {
|
type PermissionRegistry interface {
|
||||||
RegisterPluginScope(scope string)
|
RegisterPluginScope(scope string)
|
||||||
RegisterPermission(action, scope string)
|
RegisterPermission(action, scope string) error
|
||||||
IsPermissionValid(action, scope string) error
|
IsPermissionValid(action, scope string) error
|
||||||
GetScopePrefixes(action string) (PrefixSet, bool)
|
GetScopePrefixes(action string) (PrefixSet, bool)
|
||||||
}
|
}
|
||||||
@@ -87,6 +93,7 @@ func newPermissionRegistry() *permissionRegistry {
|
|||||||
"global.users": "global.users:id:",
|
"global.users": "global.users:id:",
|
||||||
"roles": "roles:uid:",
|
"roles": "roles:uid:",
|
||||||
"services": "services:",
|
"services": "services:",
|
||||||
|
"receivers": "receivers:uid:",
|
||||||
}
|
}
|
||||||
return &permissionRegistry{
|
return &permissionRegistry{
|
||||||
actionScopePrefixes: make(map[string]PrefixSet, 200),
|
actionScopePrefixes: make(map[string]PrefixSet, 200),
|
||||||
@@ -101,42 +108,47 @@ func (pr *permissionRegistry) RegisterPluginScope(scope string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scopeParts := strings.Split(scope, ":")
|
scopeParts := strings.Split(scope, ":")
|
||||||
|
kind := scopeParts[0]
|
||||||
|
|
||||||
// If the scope is already registered, return
|
// If the scope is already registered, return
|
||||||
if _, found := pr.kindScopePrefix[scopeParts[0]]; found {
|
if _, found := pr.kindScopePrefix[kind]; found {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the scope contains an attribute part, register the kind and attribute
|
// If the scope contains an attribute part, register the kind and attribute
|
||||||
if len(scopeParts) > 2 {
|
if len(scopeParts) > 2 {
|
||||||
kind, attr := scopeParts[0], scopeParts[1]
|
attr := scopeParts[1]
|
||||||
pr.kindScopePrefix[kind] = kind + ":" + attr + ":"
|
pr.kindScopePrefix[kind] = kind + ":" + attr + ":"
|
||||||
pr.logger.Debug("registered scope prefix", "kind", kind, "scope_prefix", kind+":"+attr+":")
|
pr.logger.Debug("registered scope prefix", "kind", kind, "scope_prefix", kind+":"+attr+":")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.logger.Debug("registered scope prefix", "kind", scopeParts[0], "scope_prefix", scopeParts[0]+":")
|
pr.logger.Debug("registered scope prefix", "kind", kind, "scope_prefix", kind+":")
|
||||||
pr.kindScopePrefix[scopeParts[0]] = scopeParts[0] + ":"
|
pr.kindScopePrefix[kind] = kind + ":"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pr *permissionRegistry) RegisterPermission(action, scope string) {
|
func (pr *permissionRegistry) RegisterPermission(action, scope string) error {
|
||||||
if _, ok := pr.actionScopePrefixes[action]; !ok {
|
if _, ok := pr.actionScopePrefixes[action]; ok {
|
||||||
pr.actionScopePrefixes[action] = PrefixSet{}
|
// action already registered
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
pr.actionScopePrefixes[action] = PrefixSet{}
|
||||||
|
|
||||||
if scope == "" {
|
if scope == "" {
|
||||||
// scopeless action
|
// scopeless action
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
kind := strings.Split(scope, ":")[0]
|
kind := strings.Split(scope, ":")[0]
|
||||||
scopePrefix, ok := pr.kindScopePrefix[kind]
|
scopePrefix, ok := pr.kindScopePrefix[kind]
|
||||||
if !ok {
|
if !ok {
|
||||||
pr.logger.Warn("unknown scope prefix", "scope", scope)
|
pr.logger.Error("unknown kind: please update `kindScopePrefix` with the correct scope prefix", "kind", kind)
|
||||||
return
|
return ErrUnknownKind(kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new entry in case the scope is not empty
|
// Add a new entry in case the scope is not empty
|
||||||
pr.actionScopePrefixes[action][scopePrefix] = true
|
pr.actionScopePrefixes[action][scopePrefix] = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pr *permissionRegistry) IsPermissionValid(action, scope string) error {
|
func (pr *permissionRegistry) IsPermissionValid(action, scope string) error {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func Test_permissionRegistry_RegisterPermission(t *testing.T) {
|
|||||||
scope string
|
scope string
|
||||||
wantKind string
|
wantKind string
|
||||||
wantPrefixSet PrefixSet
|
wantPrefixSet PrefixSet
|
||||||
wantSkip bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "register folders read",
|
name: "register folders read",
|
||||||
@@ -67,16 +67,31 @@ func Test_permissionRegistry_RegisterPermission(t *testing.T) {
|
|||||||
wantPrefixSet: PrefixSet{},
|
wantPrefixSet: PrefixSet{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "register an action on an unknown kind",
|
name: "register an action on an unknown kind",
|
||||||
action: "unknown:action",
|
action: "unknown:action",
|
||||||
scope: "unknown:uid:*",
|
scope: "unknown:uid:*",
|
||||||
wantPrefixSet: PrefixSet{},
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "register an action that is already registered",
|
||||||
|
action: "already:registered",
|
||||||
|
scope: "already:uid:*",
|
||||||
|
wantPrefixSet: PrefixSet{"already:uid:": true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
pr := newPermissionRegistry()
|
pr := newPermissionRegistry()
|
||||||
pr.RegisterPermission(tt.action, tt.scope)
|
|
||||||
|
// Pretend that an action is registered
|
||||||
|
pr.actionScopePrefixes["already:registered"] = PrefixSet{"already:uid:": true}
|
||||||
|
|
||||||
|
err := pr.RegisterPermission(tt.action, tt.scope)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
got, ok := pr.actionScopePrefixes[tt.action]
|
got, ok := pr.actionScopePrefixes[tt.action]
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
for k, v := range got {
|
for k, v := range got {
|
||||||
@@ -88,8 +103,10 @@ func Test_permissionRegistry_RegisterPermission(t *testing.T) {
|
|||||||
|
|
||||||
func Test_permissionRegistry_IsPermissionValid(t *testing.T) {
|
func Test_permissionRegistry_IsPermissionValid(t *testing.T) {
|
||||||
pr := newPermissionRegistry()
|
pr := newPermissionRegistry()
|
||||||
pr.RegisterPermission("folders:read", "folders:uid:")
|
err := pr.RegisterPermission("folders:read", "folders:uid:")
|
||||||
pr.RegisterPermission("test-app.settings:read", "")
|
require.NoError(t, err)
|
||||||
|
err = pr.RegisterPermission("test-app.settings:read", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -166,8 +183,10 @@ func Test_permissionRegistry_IsPermissionValid(t *testing.T) {
|
|||||||
|
|
||||||
func Test_permissionRegistry_GetScopePrefixes(t *testing.T) {
|
func Test_permissionRegistry_GetScopePrefixes(t *testing.T) {
|
||||||
pr := newPermissionRegistry()
|
pr := newPermissionRegistry()
|
||||||
pr.RegisterPermission("folders:read", "folders:uid:")
|
err := pr.RegisterPermission("folders:read", "folders:uid:")
|
||||||
pr.RegisterPermission("test-app.settings:read", "")
|
require.NoError(t, err)
|
||||||
|
err = pr.RegisterPermission("test-app.settings:read", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -1,22 +1,38 @@
|
|||||||
package test
|
package test
|
||||||
|
|
||||||
import "github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
func ProvidePermissionRegistry() permreg.PermissionRegistry {
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProvidePermissionRegistry(t *testing.T) permreg.PermissionRegistry {
|
||||||
permReg := permreg.ProvidePermissionRegistry()
|
permReg := permreg.ProvidePermissionRegistry()
|
||||||
// Test core permissions
|
// Test core permissions
|
||||||
permReg.RegisterPermission("datasources:read", "datasources:uid:")
|
err := permReg.RegisterPermission("datasources:read", "datasources:uid:")
|
||||||
permReg.RegisterPermission("dashboards:read", "dashboards:uid:")
|
require.NoError(t, err)
|
||||||
permReg.RegisterPermission("dashboards:read", "folders:uid:")
|
err = permReg.RegisterPermission("dashboards:read", "dashboards:uid:")
|
||||||
permReg.RegisterPermission("folders:read", "folders:uid:")
|
require.NoError(t, err)
|
||||||
|
err = permReg.RegisterPermission("dashboards:read", "folders:uid:")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = permReg.RegisterPermission("folders:read", "folders:uid:")
|
||||||
|
require.NoError(t, err)
|
||||||
// Test plugins permissions
|
// Test plugins permissions
|
||||||
permReg.RegisterPermission("plugins.app:access", "plugins:id:")
|
err = permReg.RegisterPermission("plugins.app:access", "plugins:id:")
|
||||||
|
require.NoError(t, err)
|
||||||
// App
|
// App
|
||||||
permReg.RegisterPermission("test-app:read", "")
|
err = permReg.RegisterPermission("test-app:read", "")
|
||||||
permReg.RegisterPermission("test-app.settings:read", "")
|
require.NoError(t, err)
|
||||||
permReg.RegisterPermission("test-app.projects:read", "")
|
err = permReg.RegisterPermission("test-app.settings:read", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = permReg.RegisterPermission("test-app.projects:read", "")
|
||||||
|
require.NoError(t, err)
|
||||||
// App 1
|
// App 1
|
||||||
permReg.RegisterPermission("test-app1.catalog:read", "")
|
err = permReg.RegisterPermission("test-app1.catalog:read", "")
|
||||||
permReg.RegisterPermission("test-app1.announcements:read", "")
|
require.NoError(t, err)
|
||||||
|
err = permReg.RegisterPermission("test-app1.announcements:read", "")
|
||||||
|
require.NoError(t, err)
|
||||||
return permReg
|
return permReg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
|
func (s *Service) GetPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.GetPermissions")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.GetPermissions")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
var inheritedScopes []string
|
var inheritedScopes []string
|
||||||
@@ -209,7 +209,7 @@ func (s *Service) GetPermissions(ctx context.Context, user identity.Requester, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.SetUserPermission")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetUserPermission")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
actions, err := s.mapPermission(permission)
|
actions, err := s.mapPermission(permission)
|
||||||
@@ -235,7 +235,7 @@ func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user acces
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.SetTeamPermission")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetTeamPermission")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
actions, err := s.mapPermission(permission)
|
actions, err := s.mapPermission(permission)
|
||||||
@@ -261,7 +261,7 @@ func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
func (s *Service) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.SetBuiltInRolePermission")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetBuiltInRolePermission")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
actions, err := s.mapPermission(permission)
|
actions, err := s.mapPermission(permission)
|
||||||
@@ -290,7 +290,7 @@ func (s *Service) SetPermissions(
|
|||||||
ctx context.Context, orgID int64, resourceID string,
|
ctx context.Context, orgID int64, resourceID string,
|
||||||
commands ...accesscontrol.SetResourcePermissionCommand,
|
commands ...accesscontrol.SetResourcePermissionCommand,
|
||||||
) ([]accesscontrol.ResourcePermission, error) {
|
) ([]accesscontrol.ResourcePermission, error) {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.SetPermissions")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.SetPermissions")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if err := s.validateResource(ctx, orgID, resourceID); err != nil {
|
if err := s.validateResource(ctx, orgID, resourceID); err != nil {
|
||||||
@@ -370,7 +370,7 @@ func (s *Service) mapPermission(permission string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) validateResource(ctx context.Context, orgID int64, resourceID string) error {
|
func (s *Service) validateResource(ctx context.Context, orgID int64, resourceID string) error {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.validateResource")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateResource")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if s.options.ResourceValidator != nil {
|
if s.options.ResourceValidator != nil {
|
||||||
@@ -380,7 +380,7 @@ func (s *Service) validateResource(ctx context.Context, orgID int64, resourceID
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) validateUser(ctx context.Context, orgID, userID int64) error {
|
func (s *Service) validateUser(ctx context.Context, orgID, userID int64) error {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.validateUser")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateUser")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if !s.options.Assignments.Users {
|
if !s.options.Assignments.Users {
|
||||||
@@ -397,7 +397,7 @@ func (s *Service) validateUser(ctx context.Context, orgID, userID int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) validateTeam(ctx context.Context, orgID, teamID int64) error {
|
func (s *Service) validateTeam(ctx context.Context, orgID, teamID int64) error {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.validateTeam")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateTeam")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if !s.options.Assignments.Teams {
|
if !s.options.Assignments.Teams {
|
||||||
@@ -416,7 +416,7 @@ func (s *Service) validateTeam(ctx context.Context, orgID, teamID int64) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) validateBuiltinRole(ctx context.Context, builtinRole string) error {
|
func (s *Service) validateBuiltinRole(ctx context.Context, builtinRole string) error {
|
||||||
_, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.validateBuiltinRole")
|
_, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.validateBuiltinRole")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if !s.options.Assignments.BuiltInRoles {
|
if !s.options.Assignments.BuiltInRoles {
|
||||||
@@ -580,7 +580,7 @@ func (a *ActionSetSvc) ExpandActionSetsWithFilter(permissions []accesscontrol.Pe
|
|||||||
// RegisterActionSets allow the caller to expand the existing action sets with additional permissions
|
// RegisterActionSets allow the caller to expand the existing action sets with additional permissions
|
||||||
// This is intended to be used by plugins, and currently supports extending folder and dashboard action sets
|
// This is intended to be used by plugins, and currently supports extending folder and dashboard action sets
|
||||||
func (a *ActionSetSvc) RegisterActionSets(ctx context.Context, pluginID string, registrations []plugins.ActionSet) error {
|
func (a *ActionSetSvc) RegisterActionSets(ctx context.Context, pluginID string, registrations []plugins.ActionSet) error {
|
||||||
ctx, span := tracer.Start(ctx, "accesscontroll.resourcepermissions.RegisterActionSets")
|
ctx, span := tracer.Start(ctx, "accesscontrol.resourcepermissions.RegisterActionSets")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) || !a.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
|
if !a.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) || !a.features.IsEnabled(ctx, featuremgmt.FlagAccessControlOnCall) {
|
||||||
|
|||||||
@@ -17,6 +17,21 @@ type NamespaceMapper = claims.NamespaceFormatter
|
|||||||
|
|
||||||
// GetNamespaceMapper returns a function that will convert orgIds into a consistent namespace
|
// GetNamespaceMapper returns a function that will convert orgIds into a consistent namespace
|
||||||
func GetNamespaceMapper(cfg *setting.Cfg) NamespaceMapper {
|
func GetNamespaceMapper(cfg *setting.Cfg) NamespaceMapper {
|
||||||
|
if cfg != nil && cfg.StackID != "" {
|
||||||
|
stackId, err := strconv.ParseInt(cfg.StackID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
stackId = 0
|
||||||
|
}
|
||||||
|
// Temporarily force this as plural
|
||||||
|
cloudNamespace := fmt.Sprintf("stacks-%d", stackId)
|
||||||
|
// cloudNamespace := claims.CloudNamespaceFormatter(stackIdInt)
|
||||||
|
return func(_ int64) string { return cloudNamespace }
|
||||||
|
}
|
||||||
|
return claims.OrgNamespaceFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary version that is only passed to th
|
||||||
|
func GetTemporarySingularNamespaceMapper(cfg *setting.Cfg) NamespaceMapper {
|
||||||
if cfg != nil && cfg.StackID != "" {
|
if cfg != nil && cfg.StackID != "" {
|
||||||
stackIdInt, err := strconv.ParseInt(cfg.StackID, 10, 64)
|
stackIdInt, err := strconv.ParseInt(cfg.StackID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ func TestNamespaceMapper(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "with stackId",
|
name: "with stackId",
|
||||||
cfg: "abc",
|
cfg: "abc",
|
||||||
orgId: 123, // ignored
|
orgId: 123, // ignored
|
||||||
expected: "stack-0", // we parse to int and default to 0
|
expected: "stacks-0", // we parse to int and default to 0
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-jose/go-jose/v3/jwt"
|
"github.com/go-jose/go-jose/v3/jwt"
|
||||||
authnlib "github.com/grafana/authlib/authn"
|
authnlib "github.com/grafana/authlib/authn"
|
||||||
authnlibclaims "github.com/grafana/authlib/claims"
|
"github.com/grafana/authlib/claims"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"golang.org/x/sync/singleflight"
|
"golang.org/x/sync/singleflight"
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ func ProvideService(
|
|||||||
cfg: cfg, logger: log.New("id-service"),
|
cfg: cfg, logger: log.New("id-service"),
|
||||||
signer: signer, cache: cache,
|
signer: signer, cache: cache,
|
||||||
metrics: newMetrics(reg),
|
metrics: newMetrics(reg),
|
||||||
nsMapper: request.GetNamespaceMapper(cfg),
|
nsMapper: request.GetTemporarySingularNamespaceMapper(cfg), // TODO replace with the plural one
|
||||||
}
|
}
|
||||||
|
|
||||||
authnService.RegisterPostAuthHook(s.hook, 140)
|
authnService.RegisterPostAuthHook(s.hook, 140)
|
||||||
@@ -85,7 +85,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
|
|||||||
s.logger.FromContext(ctx).Debug("Sign new id token", "id", id.GetID())
|
s.logger.FromContext(ctx).Debug("Sign new id token", "id", id.GetID())
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
claims := &auth.IDClaims{
|
idClaims := &auth.IDClaims{
|
||||||
Claims: &jwt.Claims{
|
Claims: &jwt.Claims{
|
||||||
Issuer: s.cfg.AppURL,
|
Issuer: s.cfg.AppURL,
|
||||||
Audience: getAudience(id.GetOrgID()),
|
Audience: getAudience(id.GetOrgID()),
|
||||||
@@ -100,15 +100,15 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if id.IsIdentityType(authnlibclaims.TypeUser) {
|
if id.IsIdentityType(claims.TypeUser) {
|
||||||
claims.Rest.Email = id.GetEmail()
|
idClaims.Rest.Email = id.GetEmail()
|
||||||
claims.Rest.EmailVerified = id.IsEmailVerified()
|
idClaims.Rest.EmailVerified = id.IsEmailVerified()
|
||||||
claims.Rest.AuthenticatedBy = id.GetAuthenticatedBy()
|
idClaims.Rest.AuthenticatedBy = id.GetAuthenticatedBy()
|
||||||
claims.Rest.Username = id.GetLogin()
|
idClaims.Rest.Username = id.GetLogin()
|
||||||
claims.Rest.DisplayName = id.GetDisplayName()
|
idClaims.Rest.DisplayName = id.GetDisplayName()
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := s.signer.SignIDToken(ctx, claims)
|
token, err := s.signer.SignIDToken(ctx, idClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.metrics.failedTokenSigningCounter.Inc()
|
s.metrics.failedTokenSigningCounter.Inc()
|
||||||
return resultType{}, nil
|
return resultType{}, nil
|
||||||
@@ -124,7 +124,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
|
|||||||
s.logger.FromContext(ctx).Error("Failed to add id token to cache", "error", err)
|
s.logger.FromContext(ctx).Error("Failed to add id token to cache", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultType{token: token, idClaims: claims}, nil
|
return resultType{token: token, idClaims: idClaims}, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -140,7 +140,7 @@ func (s *Service) RemoveIDToken(ctx context.Context, id identity.Requester) erro
|
|||||||
|
|
||||||
func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
|
func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
|
||||||
// FIXME(kalleep): we should probably lazy load this
|
// FIXME(kalleep): we should probably lazy load this
|
||||||
token, claims, err := s.SignIdentity(ctx, identity)
|
token, idClaims, err := s.SignIdentity(ctx, identity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if shouldLogErr(err) {
|
if shouldLogErr(err) {
|
||||||
s.logger.FromContext(ctx).Error("Failed to sign id token", "err", err, "id", identity.GetID())
|
s.logger.FromContext(ctx).Error("Failed to sign id token", "err", err, "id", identity.GetID())
|
||||||
@@ -150,7 +150,7 @@ func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
identity.IDToken = token
|
identity.IDToken = token
|
||||||
identity.IDTokenClaims = claims
|
identity.IDTokenClaims = idClaims
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ var _ authn.HookClient = new(APIKey)
|
|||||||
var _ authn.ContextAwareClient = new(APIKey)
|
var _ authn.ContextAwareClient = new(APIKey)
|
||||||
var _ authn.IdentityResolverClient = new(APIKey)
|
var _ authn.IdentityResolverClient = new(APIKey)
|
||||||
|
|
||||||
|
const (
|
||||||
|
metaKeyID = "keyID"
|
||||||
|
metaKeySkipLastUsed = "keySkipLastUsed"
|
||||||
|
)
|
||||||
|
|
||||||
func ProvideAPIKey(apiKeyService apikey.Service) *APIKey {
|
func ProvideAPIKey(apiKeyService apikey.Service) *APIKey {
|
||||||
return &APIKey{
|
return &APIKey{
|
||||||
log: log.New(authn.ClientAPIKey),
|
log: log.New(authn.ClientAPIKey),
|
||||||
@@ -64,6 +69,14 @@ func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set keyID so we can use it in last used hook
|
||||||
|
r.SetMeta(metaKeyID, strconv.FormatInt(key.ID, 10))
|
||||||
|
if !shouldUpdateLastUsedAt(key) {
|
||||||
|
// Hack to just have some value, we will check this key in the hook
|
||||||
|
// and if its not an empty string we will not update last used.
|
||||||
|
r.SetMeta(metaKeySkipLastUsed, "true")
|
||||||
|
}
|
||||||
|
|
||||||
// if the api key don't belong to a service account construct the identity and return it
|
// if the api key don't belong to a service account construct the identity and return it
|
||||||
if key.ServiceAccountId == nil || *key.ServiceAccountId < 1 {
|
if key.ServiceAccountId == nil || *key.ServiceAccountId < 1 {
|
||||||
return newAPIKeyIdentity(key), nil
|
return newAPIKeyIdentity(key), nil
|
||||||
@@ -109,7 +122,6 @@ func (s *APIKey) getFromTokenLegacy(ctx context.Context, token string) (*apikey.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch key
|
// fetch key
|
||||||
keyQuery := apikey.GetByNameQuery{KeyName: decoded.Name, OrgID: decoded.OrgId}
|
keyQuery := apikey.GetByNameQuery{KeyName: decoded.Name, OrgID: decoded.OrgId}
|
||||||
key, err := s.apiKeyService.GetApiKeyByName(ctx, &keyQuery)
|
key, err := s.apiKeyService.GetApiKeyByName(ctx, &keyQuery)
|
||||||
@@ -170,52 +182,32 @@ func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, typ claims.Id
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
|
func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
|
||||||
id, exists := s.getAPIKeyID(ctx, identity, r)
|
if r.GetMeta(metaKeySkipLastUsed) != "" {
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
go func(apikeyID int64) {
|
go func(keyID string) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
s.log.Error("Panic during user last seen sync", "err", err)
|
s.log.Error("Panic during user last seen sync", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err := s.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), apikeyID); err != nil {
|
|
||||||
s.log.Warn("Failed to update last use date for api key", "id", apikeyID)
|
id, err := strconv.ParseInt(keyID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warn("Invalid api key id", "id", keyID, "err", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}(id)
|
|
||||||
|
if err := s.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), id); err != nil {
|
||||||
|
s.log.Warn("Failed to update last used date for api key", "id", keyID, "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(r.GetMeta(metaKeyID))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIKey) getAPIKeyID(ctx context.Context, id *authn.Identity, r *authn.Request) (apiKeyID int64, exists bool) {
|
|
||||||
internalId, err := id.GetInternalID()
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warn("Failed to parse ID from identifier", "err", err)
|
|
||||||
return -1, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if id.IsIdentityType(claims.TypeAPIKey) {
|
|
||||||
return internalId, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if id.IsIdentityType(claims.TypeServiceAccount) {
|
|
||||||
// When the identity is service account, the ID in from the namespace is the service account ID.
|
|
||||||
// We need to fetch the API key in this scenario, as we could use it to uniquely identify a service account token.
|
|
||||||
apiKey, err := s.getAPIKey(ctx, getTokenFromRequest(r))
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warn("Failed to fetch the API Key from request")
|
|
||||||
return -1, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiKey.ID, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func looksLikeApiKey(token string) bool {
|
func looksLikeApiKey(token string) bool {
|
||||||
return token != ""
|
return token != ""
|
||||||
}
|
}
|
||||||
@@ -276,3 +268,7 @@ func newServiceAccountIdentity(key *apikey.APIKey) *authn.Identity {
|
|||||||
ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true},
|
ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldUpdateLastUsedAt(key *apikey.APIKey) bool {
|
||||||
|
return key.LastUsedAt == nil || time.Since(*key.LastUsedAt) > 5*time.Minute
|
||||||
|
}
|
||||||
|
|||||||
@@ -189,108 +189,6 @@ func TestAPIKey_Test(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) {
|
|
||||||
type TestCase struct {
|
|
||||||
desc string
|
|
||||||
expectedKey *apikey.APIKey
|
|
||||||
expectedIdentity *authn.Identity
|
|
||||||
expectedError error
|
|
||||||
expectedKeyID int64
|
|
||||||
expectedExists bool
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []TestCase{
|
|
||||||
{
|
|
||||||
desc: "should return API Key ID for valid token that is connected to service account",
|
|
||||||
expectedKey: &apikey.APIKey{
|
|
||||||
ID: 1,
|
|
||||||
OrgID: 1,
|
|
||||||
Key: hash,
|
|
||||||
ServiceAccountId: intPtr(1),
|
|
||||||
},
|
|
||||||
expectedIdentity: &authn.Identity{
|
|
||||||
ID: "1",
|
|
||||||
Type: claims.TypeServiceAccount,
|
|
||||||
OrgID: 1,
|
|
||||||
Name: "test",
|
|
||||||
AuthenticatedBy: login.APIKeyAuthModule,
|
|
||||||
},
|
|
||||||
expectedKeyID: 1,
|
|
||||||
expectedExists: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should return API Key ID for valid token for API key",
|
|
||||||
expectedKey: &apikey.APIKey{
|
|
||||||
ID: 2,
|
|
||||||
OrgID: 1,
|
|
||||||
Key: hash,
|
|
||||||
},
|
|
||||||
expectedIdentity: &authn.Identity{
|
|
||||||
ID: "2",
|
|
||||||
Type: claims.TypeAPIKey,
|
|
||||||
OrgID: 1,
|
|
||||||
Name: "test",
|
|
||||||
AuthenticatedBy: login.APIKeyAuthModule,
|
|
||||||
},
|
|
||||||
expectedKeyID: 2,
|
|
||||||
expectedExists: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should not return any ID when the request is not made by API key or service account",
|
|
||||||
expectedKey: &apikey.APIKey{
|
|
||||||
ID: 2,
|
|
||||||
OrgID: 1,
|
|
||||||
Key: hash,
|
|
||||||
},
|
|
||||||
expectedIdentity: &authn.Identity{
|
|
||||||
ID: "2",
|
|
||||||
Type: claims.TypeUser,
|
|
||||||
OrgID: 1,
|
|
||||||
Name: "test",
|
|
||||||
AuthenticatedBy: login.APIKeyAuthModule,
|
|
||||||
},
|
|
||||||
expectedKeyID: -1,
|
|
||||||
expectedExists: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "should not return any ID when the can't fetch API Key",
|
|
||||||
expectedKey: &apikey.APIKey{
|
|
||||||
ID: 1,
|
|
||||||
OrgID: 1,
|
|
||||||
Key: hash,
|
|
||||||
},
|
|
||||||
expectedIdentity: &authn.Identity{
|
|
||||||
ID: "2",
|
|
||||||
Type: claims.TypeServiceAccount,
|
|
||||||
OrgID: 1,
|
|
||||||
Name: "test",
|
|
||||||
AuthenticatedBy: login.APIKeyAuthModule,
|
|
||||||
},
|
|
||||||
expectedError: fmt.Errorf("invalid token"),
|
|
||||||
expectedKeyID: -1,
|
|
||||||
expectedExists: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &authn.Request{HTTPRequest: &http.Request{
|
|
||||||
Header: map[string][]string{
|
|
||||||
"Authorization": {"Bearer " + secret},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.desc, func(t *testing.T) {
|
|
||||||
c := ProvideAPIKey(&apikeytest.Service{
|
|
||||||
ExpectedError: tt.expectedError,
|
|
||||||
ExpectedAPIKey: tt.expectedKey,
|
|
||||||
})
|
|
||||||
id, exists := c.getAPIKeyID(context.Background(), tt.expectedIdentity, req)
|
|
||||||
assert.Equal(t, tt.expectedExists, exists)
|
|
||||||
assert.Equal(t, tt.expectedKeyID, id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIKey_ResolveIdentity(t *testing.T) {
|
func TestAPIKey_ResolveIdentity(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
desc string
|
desc string
|
||||||
|
|||||||
@@ -74,13 +74,11 @@ type ExtendedJWT struct {
|
|||||||
func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
||||||
jwtToken := s.retrieveAuthenticationToken(r.HTTPRequest)
|
jwtToken := s.retrieveAuthenticationToken(r.HTTPRequest)
|
||||||
|
|
||||||
accessToken, err := s.accessTokenVerifier.Verify(ctx, jwtToken)
|
accessTokenClaims, err := s.accessTokenVerifier.Verify(ctx, jwtToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errExtJWTInvalid.Errorf("failed to verify access token: %w", err)
|
return nil, errExtJWTInvalid.Errorf("failed to verify access token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessTokenClaims := authlib.NewAccessClaims(*accessToken)
|
|
||||||
|
|
||||||
idToken := s.retrieveAuthorizationToken(r.HTTPRequest)
|
idToken := s.retrieveAuthorizationToken(r.HTTPRequest)
|
||||||
if idToken != "" {
|
if idToken != "" {
|
||||||
idTokenClaims, err := s.idTokenVerifier.Verify(ctx, idToken)
|
idTokenClaims, err := s.idTokenVerifier.Verify(ctx, idToken)
|
||||||
@@ -88,10 +86,10 @@ func (s *ExtendedJWT) Authenticate(ctx context.Context, r *authn.Request) (*auth
|
|||||||
return nil, errExtJWTInvalid.Errorf("failed to verify id token: %w", err)
|
return nil, errExtJWTInvalid.Errorf("failed to verify id token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.authenticateAsUser(authlib.NewIdentityClaims(*idTokenClaims), accessTokenClaims)
|
return s.authenticateAsUser(*idTokenClaims, *accessTokenClaims)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.authenticateAsService(accessTokenClaims)
|
return s.authenticateAsService(*accessTokenClaims)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExtendedJWT) IsEnabled() bool {
|
func (s *ExtendedJWT) IsEnabled() bool {
|
||||||
@@ -99,73 +97,75 @@ func (s *ExtendedJWT) IsEnabled() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExtendedJWT) authenticateAsUser(
|
func (s *ExtendedJWT) authenticateAsUser(
|
||||||
idTokenClaims claims.IdentityClaims,
|
idTokenClaims authlib.Claims[authlib.IDTokenClaims],
|
||||||
accessTokenClaims claims.AccessClaims,
|
accessTokenClaims authlib.Claims[authlib.AccessTokenClaims],
|
||||||
) (*authn.Identity, error) {
|
) (*authn.Identity, error) {
|
||||||
// Only allow id tokens signed for namespace configured for this instance.
|
// Only allow id tokens signed for namespace configured for this instance.
|
||||||
if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(idTokenClaims, allowedNamespace) {
|
if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(authlib.NewIdentityClaims(idTokenClaims), allowedNamespace) {
|
||||||
return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected id token namespace: %s", idTokenClaims.Namespace())
|
return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected id token namespace: %s", idTokenClaims.Rest.Namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow access tokens with either the same namespace as the validated id token namespace or wildcard (`*`).
|
// Allow access tokens with either the same namespace as the validated id token namespace or wildcard (`*`).
|
||||||
if !claims.NamespaceMatches(accessTokenClaims, idTokenClaims.Namespace()) {
|
if !claims.NamespaceMatches(authlib.NewAccessClaims(accessTokenClaims), idTokenClaims.Rest.Namespace) {
|
||||||
return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Namespace())
|
return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.Namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
accessType, _, err := identity.ParseTypeAndID(accessTokenClaims.Subject())
|
accessType, _, err := identity.ParseTypeAndID(accessTokenClaims.Subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject())
|
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !claims.IsIdentityType(accessType, claims.TypeAccessPolicy) {
|
if !claims.IsIdentityType(accessType, claims.TypeAccessPolicy) {
|
||||||
return nil, errExtJWTInvalid.Errorf("unexpected identity: %s", accessTokenClaims.Subject())
|
return nil, errExtJWTInvalid.Errorf("unexpected identity: %s", accessTokenClaims.Subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
t, id, err := identity.ParseTypeAndID(idTokenClaims.Subject())
|
t, id, err := identity.ParseTypeAndID(idTokenClaims.Subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errExtJWTInvalid.Errorf("failed to parse id token subject: %w", err)
|
return nil, errExtJWTInvalid.Errorf("failed to parse id token subject: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !claims.IsIdentityType(t, claims.TypeUser) {
|
if !claims.IsIdentityType(t, claims.TypeUser) {
|
||||||
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject())
|
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For use in service layer, allow higher privilege
|
// For use in service layer, allow higher privilege
|
||||||
allowedKubernetesNamespace := accessTokenClaims.Namespace()
|
allowedKubernetesNamespace := accessTokenClaims.Rest.Namespace
|
||||||
if len(s.cfg.StackID) > 0 {
|
if len(s.cfg.StackID) > 0 {
|
||||||
// For single-tenant cloud use, choose the lower of the two (id token will always have the specific namespace)
|
// For single-tenant cloud use, choose the lower of the two (id token will always have the specific namespace)
|
||||||
allowedKubernetesNamespace = idTokenClaims.Namespace()
|
allowedKubernetesNamespace = idTokenClaims.Rest.Namespace
|
||||||
}
|
}
|
||||||
|
|
||||||
return &authn.Identity{
|
return &authn.Identity{
|
||||||
ID: id,
|
ID: id,
|
||||||
Type: t,
|
Type: t,
|
||||||
OrgID: s.getDefaultOrgID(),
|
OrgID: s.getDefaultOrgID(),
|
||||||
|
AccessTokenClaims: &accessTokenClaims,
|
||||||
|
IDTokenClaims: &idTokenClaims,
|
||||||
AuthenticatedBy: login.ExtendedJWTModule,
|
AuthenticatedBy: login.ExtendedJWTModule,
|
||||||
AuthID: accessTokenClaims.Subject(),
|
AuthID: accessTokenClaims.Subject,
|
||||||
AllowedKubernetesNamespace: allowedKubernetesNamespace,
|
AllowedKubernetesNamespace: allowedKubernetesNamespace,
|
||||||
ClientParams: authn.ClientParams{
|
ClientParams: authn.ClientParams{
|
||||||
SyncPermissions: true,
|
SyncPermissions: true,
|
||||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||||
ActionsLookup: accessTokenClaims.DelegatedPermissions(),
|
ActionsLookup: accessTokenClaims.Rest.DelegatedPermissions,
|
||||||
},
|
},
|
||||||
FetchSyncedUser: true,
|
FetchSyncedUser: true,
|
||||||
}}, nil
|
}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ExtendedJWT) authenticateAsService(accessTokenClaims claims.AccessClaims) (*authn.Identity, error) {
|
func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) {
|
||||||
// Allow access tokens with that has a wildcard namespace or a namespace matching this instance.
|
// Allow access tokens with that has a wildcard namespace or a namespace matching this instance.
|
||||||
if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(accessTokenClaims, allowedNamespace) {
|
if allowedNamespace := s.namespaceMapper(s.getDefaultOrgID()); !claims.NamespaceMatches(authlib.NewAccessClaims(accessTokenClaims), allowedNamespace) {
|
||||||
return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", accessTokenClaims.Namespace())
|
return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.Namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
t, id, err := identity.ParseTypeAndID(accessTokenClaims.Subject())
|
t, id, err := identity.ParseTypeAndID(accessTokenClaims.Subject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse access token subject: %w", err)
|
return nil, fmt.Errorf("failed to parse access token subject: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !claims.IsIdentityType(t, claims.TypeAccessPolicy) {
|
if !claims.IsIdentityType(t, claims.TypeAccessPolicy) {
|
||||||
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject())
|
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessTokenClaims.Subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &authn.Identity{
|
return &authn.Identity{
|
||||||
@@ -173,13 +173,15 @@ func (s *ExtendedJWT) authenticateAsService(accessTokenClaims claims.AccessClaim
|
|||||||
UID: id,
|
UID: id,
|
||||||
Type: t,
|
Type: t,
|
||||||
OrgID: s.getDefaultOrgID(),
|
OrgID: s.getDefaultOrgID(),
|
||||||
|
AccessTokenClaims: &accessTokenClaims,
|
||||||
|
IDTokenClaims: nil,
|
||||||
AuthenticatedBy: login.ExtendedJWTModule,
|
AuthenticatedBy: login.ExtendedJWTModule,
|
||||||
AuthID: accessTokenClaims.Subject(),
|
AuthID: accessTokenClaims.Subject,
|
||||||
AllowedKubernetesNamespace: accessTokenClaims.Namespace(),
|
AllowedKubernetesNamespace: accessTokenClaims.Rest.Namespace,
|
||||||
ClientParams: authn.ClientParams{
|
ClientParams: authn.ClientParams{
|
||||||
SyncPermissions: true,
|
SyncPermissions: true,
|
||||||
FetchPermissionsParams: authn.FetchPermissionsParams{
|
FetchPermissionsParams: authn.FetchPermissionsParams{
|
||||||
Roles: accessTokenClaims.Permissions(),
|
Roles: accessTokenClaims.Rest.Permissions,
|
||||||
},
|
},
|
||||||
FetchSyncedUser: false,
|
FetchSyncedUser: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
UID: "this-uid",
|
UID: "this-uid",
|
||||||
Type: claims.TypeAccessPolicy,
|
Type: claims.TypeAccessPolicy,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaims,
|
||||||
AllowedKubernetesNamespace: "default",
|
AllowedKubernetesNamespace: "default",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
@@ -247,6 +248,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
UID: "this-uid",
|
UID: "this-uid",
|
||||||
Type: claims.TypeAccessPolicy,
|
Type: claims.TypeAccessPolicy,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaimsWildcard,
|
||||||
AllowedKubernetesNamespace: "*",
|
AllowedKubernetesNamespace: "*",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
@@ -264,6 +266,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
ID: "2",
|
ID: "2",
|
||||||
Type: claims.TypeUser,
|
Type: claims.TypeUser,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaims,
|
||||||
|
IDTokenClaims: &validIDTokenClaims,
|
||||||
AllowedKubernetesNamespace: "default",
|
AllowedKubernetesNamespace: "default",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
@@ -285,6 +289,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
ID: "2",
|
ID: "2",
|
||||||
Type: claims.TypeUser,
|
Type: claims.TypeUser,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaimsWildcard,
|
||||||
|
IDTokenClaims: &validIDTokenClaims,
|
||||||
AllowedKubernetesNamespace: "*",
|
AllowedKubernetesNamespace: "*",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
@@ -311,6 +317,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
ID: "2",
|
ID: "2",
|
||||||
Type: claims.TypeUser,
|
Type: claims.TypeUser,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaimsWildcard,
|
||||||
|
IDTokenClaims: &validIDTokenClaimsWithStackSet,
|
||||||
AllowedKubernetesNamespace: "stacks-1234",
|
AllowedKubernetesNamespace: "stacks-1234",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
@@ -337,6 +345,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
UID: "this-uid",
|
UID: "this-uid",
|
||||||
Type: claims.TypeAccessPolicy,
|
Type: claims.TypeAccessPolicy,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaimsWithStackSet,
|
||||||
AllowedKubernetesNamespace: "stacks-1234",
|
AllowedKubernetesNamespace: "stacks-1234",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
@@ -362,6 +371,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
UID: "this-uid",
|
UID: "this-uid",
|
||||||
Type: claims.TypeAccessPolicy,
|
Type: claims.TypeAccessPolicy,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet,
|
||||||
AllowedKubernetesNamespace: "stack-1234",
|
AllowedKubernetesNamespace: "stack-1234",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
@@ -387,6 +397,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
ID: "2",
|
ID: "2",
|
||||||
Type: claims.TypeUser,
|
Type: claims.TypeUser,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaimsWithDeprecatedStackClaimSet,
|
||||||
|
IDTokenClaims: &validIDTokenClaimsWithDeprecatedStackClaimSet,
|
||||||
AllowedKubernetesNamespace: "stack-1234",
|
AllowedKubernetesNamespace: "stack-1234",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
@@ -413,6 +425,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
|||||||
ID: "2",
|
ID: "2",
|
||||||
Type: claims.TypeUser,
|
Type: claims.TypeUser,
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
AccessTokenClaims: &validAccessTokenClaimsWildcard,
|
||||||
|
IDTokenClaims: &validIDTokenClaimsWithStackSet,
|
||||||
AllowedKubernetesNamespace: "stacks-1234",
|
AllowedKubernetesNamespace: "stacks-1234",
|
||||||
AuthenticatedBy: "extendedjwt",
|
AuthenticatedBy: "extendedjwt",
|
||||||
AuthID: "access-policy:this-uid",
|
AuthID: "access-policy:this-uid",
|
||||||
|
|||||||
@@ -74,10 +74,15 @@ type Identity struct {
|
|||||||
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
|
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
|
||||||
IDToken string
|
IDToken string
|
||||||
IDTokenClaims *authn.Claims[authn.IDTokenClaims]
|
IDTokenClaims *authn.Claims[authn.IDTokenClaims]
|
||||||
|
|
||||||
|
AccessTokenClaims *authn.Claims[authn.AccessTokenClaims]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access implements claims.AuthInfo.
|
// Access implements claims.AuthInfo.
|
||||||
func (i *Identity) GetAccess() claims.AccessClaims {
|
func (i *Identity) GetAccess() claims.AccessClaims {
|
||||||
|
if i.AccessTokenClaims != nil {
|
||||||
|
return authn.NewAccessClaims(*i.AccessTokenClaims)
|
||||||
|
}
|
||||||
return &identity.IDClaimsWrapper{Source: i}
|
return &identity.IDClaimsWrapper{Source: i}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
|
|||||||
reqContext.IsSignedIn = !reqContext.SignedInUser.IsAnonymous
|
reqContext.IsSignedIn = !reqContext.SignedInUser.IsAnonymous
|
||||||
reqContext.AllowAnonymous = reqContext.SignedInUser.IsAnonymous
|
reqContext.AllowAnonymous = reqContext.SignedInUser.IsAnonymous
|
||||||
reqContext.IsRenderCall = id.IsAuthenticatedBy(login.RenderModule)
|
reqContext.IsRenderCall = id.IsAuthenticatedBy(login.RenderModule)
|
||||||
|
ctx = identity.WithRequester(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
h.excludeSensitiveHeadersFromRequest(reqContext.Req)
|
h.excludeSensitiveHeadersFromRequest(reqContext.Req)
|
||||||
@@ -139,8 +140,7 @@ func (h *ContextHandler) Middleware(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
// End the span to make next handlers not wrapped within middleware span
|
// End the span to make next handlers not wrapped within middleware span
|
||||||
span.End()
|
span.End()
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
next.ServeHTTP(w, r.WithContext(identity.WithRequester(ctx, id)))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -38,12 +39,16 @@ var (
|
|||||||
ScopeFoldersAll = ScopeFoldersProvider.GetResourceAllScope()
|
ScopeFoldersAll = ScopeFoldersProvider.GetResourceAllScope()
|
||||||
ScopeDashboardsProvider = ac.NewScopeProvider(ScopeDashboardsRoot)
|
ScopeDashboardsProvider = ac.NewScopeProvider(ScopeDashboardsRoot)
|
||||||
ScopeDashboardsAll = ScopeDashboardsProvider.GetResourceAllScope()
|
ScopeDashboardsAll = ScopeDashboardsProvider.GetResourceAllScope()
|
||||||
|
tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboards")
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewFolderNameScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope.
|
// NewFolderNameScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope.
|
||||||
func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
||||||
prefix := ScopeFoldersProvider.GetResourceScopeName("")
|
prefix := ScopeFoldersProvider.GetResourceScopeName("")
|
||||||
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.NewFolderNameScopeResolver")
|
||||||
|
span.End()
|
||||||
|
|
||||||
if !strings.HasPrefix(scope, prefix) {
|
if !strings.HasPrefix(scope, prefix) {
|
||||||
return nil, ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
@@ -72,6 +77,9 @@ func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Se
|
|||||||
func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
||||||
prefix := ScopeFoldersProvider.GetResourceScope("")
|
prefix := ScopeFoldersProvider.GetResourceScope("")
|
||||||
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.NewFolderIDScopeResolver")
|
||||||
|
span.End()
|
||||||
|
|
||||||
if !strings.HasPrefix(scope, prefix) {
|
if !strings.HasPrefix(scope, prefix) {
|
||||||
return nil, ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
@@ -105,6 +113,9 @@ func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Serv
|
|||||||
func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
||||||
prefix := ScopeFoldersProvider.GetResourceScopeUID("")
|
prefix := ScopeFoldersProvider.GetResourceScopeUID("")
|
||||||
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.NewFolderUIDScopeResolver")
|
||||||
|
span.End()
|
||||||
|
|
||||||
if !strings.HasPrefix(scope, prefix) {
|
if !strings.HasPrefix(scope, prefix) {
|
||||||
return nil, ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
@@ -127,6 +138,9 @@ func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttrib
|
|||||||
func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
||||||
prefix := ScopeDashboardsProvider.GetResourceScope("")
|
prefix := ScopeDashboardsProvider.GetResourceScope("")
|
||||||
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.NewDashboardIDScopeResolver")
|
||||||
|
span.End()
|
||||||
|
|
||||||
if !strings.HasPrefix(scope, prefix) {
|
if !strings.HasPrefix(scope, prefix) {
|
||||||
return nil, ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
@@ -150,6 +164,9 @@ func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardServic
|
|||||||
func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) {
|
||||||
prefix := ScopeDashboardsProvider.GetResourceScopeUID("")
|
prefix := ScopeDashboardsProvider.GetResourceScopeUID("")
|
||||||
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.NewDashboardUIDScopeResolver")
|
||||||
|
span.End()
|
||||||
|
|
||||||
if !strings.HasPrefix(scope, prefix) {
|
if !strings.HasPrefix(scope, prefix) {
|
||||||
return nil, ac.ErrInvalidScope
|
return nil, ac.ErrInvalidScope
|
||||||
}
|
}
|
||||||
@@ -169,6 +186,9 @@ func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) {
|
func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.resolveDashboardScope")
|
||||||
|
span.End()
|
||||||
|
|
||||||
var folderUID string
|
var folderUID string
|
||||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
@@ -204,6 +224,9 @@ func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, org
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderSvc folder.Service) ([]string, error) {
|
func GetInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderSvc folder.Service) ([]string, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.GetInheritedScopes")
|
||||||
|
span.End()
|
||||||
|
|
||||||
if folderUID == ac.GeneralFolderUID {
|
if folderUID == ac.GeneralFolderUID {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/tag"
|
"github.com/grafana/grafana/pkg/services/tag"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboard/database")
|
||||||
|
|
||||||
type dashboardStore struct {
|
type dashboardStore struct {
|
||||||
store db.ReplDB
|
store db.ReplDB
|
||||||
cfg *setting.Cfg
|
cfg *setting.Cfg
|
||||||
@@ -69,6 +72,9 @@ func (d *dashboardStore) emitEntityEvent() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *dashboards.Dashboard, overwrite bool) (bool, error) {
|
func (d *dashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *dashboards.Dashboard, overwrite bool) (bool, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.ValidateDashboardBeforesave")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
isParentFolderChanged := false
|
isParentFolderChanged := false
|
||||||
err := d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
err := d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
var err error
|
var err error
|
||||||
@@ -93,6 +99,9 @@ func (d *dashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashbo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) {
|
func (d *dashboardStore) GetProvisionedDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetProvisionedDataByDashboardID")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var data dashboards.DashboardProvisioning
|
var data dashboards.DashboardProvisioning
|
||||||
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
_, err := sess.Where("dashboard_id = ?", dashboardID).Get(&data)
|
_, err := sess.Where("dashboard_id = ?", dashboardID).Get(&data)
|
||||||
@@ -106,6 +115,9 @@ func (d *dashboardStore) GetProvisionedDataByDashboardID(ctx context.Context, da
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) {
|
func (d *dashboardStore) GetProvisionedDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetProvisionedDataByDashboardUID")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var provisionedDashboard dashboards.DashboardProvisioning
|
var provisionedDashboard dashboards.DashboardProvisioning
|
||||||
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
var dashboard dashboards.Dashboard
|
var dashboard dashboards.Dashboard
|
||||||
@@ -129,6 +141,9 @@ func (d *dashboardStore) GetProvisionedDataByDashboardUID(ctx context.Context, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) {
|
func (d *dashboardStore) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetProvisionedDashboardData")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var result []*dashboards.DashboardProvisioning
|
var result []*dashboards.DashboardProvisioning
|
||||||
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
return sess.Where("name = ?", name).Find(&result)
|
return sess.Where("name = ?", name).Find(&result)
|
||||||
@@ -137,6 +152,9 @@ func (d *dashboardStore) GetProvisionedDashboardData(ctx context.Context, name s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand, provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) {
|
func (d *dashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand, provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.SaveProvisionedDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var result *dashboards.Dashboard
|
var result *dashboards.Dashboard
|
||||||
var err error
|
var err error
|
||||||
err = d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
err = d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
@@ -155,6 +173,9 @@ func (d *dashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd dashb
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) SaveDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) {
|
func (d *dashboardStore) SaveDashboard(ctx context.Context, cmd dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.SaveDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var result *dashboards.Dashboard
|
var result *dashboards.Dashboard
|
||||||
var err error
|
var err error
|
||||||
err = d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
err = d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
@@ -173,6 +194,9 @@ func (d *dashboardStore) SaveDashboard(ctx context.Context, cmd dashboards.SaveD
|
|||||||
// UnprovisionDashboard removes row in dashboard_provisioning for the dashboard making it seem as if manually created.
|
// UnprovisionDashboard removes row in dashboard_provisioning for the dashboard making it seem as if manually created.
|
||||||
// The dashboard will still have `created_by = -1` to see it was not created by any particular user.
|
// The dashboard will still have `created_by = -1` to see it was not created by any particular user.
|
||||||
func (d *dashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error {
|
func (d *dashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.UnprovisionDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
_, err := sess.Where("dashboard_id = ?", id).Delete(&dashboards.DashboardProvisioning{})
|
_, err := sess.Where("dashboard_id = ?", id).Delete(&dashboards.DashboardProvisioning{})
|
||||||
return err
|
return err
|
||||||
@@ -180,6 +204,9 @@ func (d *dashboardStore) UnprovisionDashboard(ctx context.Context, id int64) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error {
|
func (d *dashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.DeleteOrphanedProvisionedDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
return d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
return d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
var result []*dashboards.DashboardProvisioning
|
var result []*dashboards.DashboardProvisioning
|
||||||
|
|
||||||
@@ -205,6 +232,9 @@ func (d *dashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
|
func (d *dashboardStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.Count")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
u := "a.Map{}
|
u := "a.Map{}
|
||||||
type result struct {
|
type result struct {
|
||||||
Count int64
|
Count int64
|
||||||
@@ -500,6 +530,9 @@ func saveProvisionedData(sess *db.Session, provisioning *dashboards.DashboardPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) {
|
func (d *dashboardStore) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboardsByPluginID")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var dashboards = make([]*dashboards.Dashboard, 0)
|
var dashboards = make([]*dashboards.Dashboard, 0)
|
||||||
err := d.store.DB().WithDbSession(ctx, func(dbSession *db.Session) error {
|
err := d.store.DB().WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||||
whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + d.store.DB().GetDialect().BooleanStr(false)
|
whereExpr := "org_id=? AND plugin_id=? AND is_folder=" + d.store.DB().GetDialect().BooleanStr(false)
|
||||||
@@ -514,6 +547,9 @@ func (d *dashboardStore) GetDashboardsByPluginID(ctx context.Context, query *das
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
|
func (d *dashboardStore) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetSoftDeletedDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if orgID == 0 || uid == "" {
|
if orgID == 0 || uid == "" {
|
||||||
return nil, dashboards.ErrDashboardIdentifierNotSet
|
return nil, dashboards.ErrDashboardIdentifierNotSet
|
||||||
}
|
}
|
||||||
@@ -537,6 +573,9 @@ func (d *dashboardStore) GetSoftDeletedDashboard(ctx context.Context, orgID int6
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) RestoreDashboard(ctx context.Context, orgID int64, dashboardUID string, folder *folder.Folder) error {
|
func (d *dashboardStore) RestoreDashboard(ctx context.Context, orgID int64, dashboardUID string, folder *folder.Folder) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.RestoreDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
if folder == nil || folder.UID == "" {
|
if folder == nil || folder.UID == "" {
|
||||||
_, err := sess.Exec("UPDATE dashboard SET deleted=NULL, folder_id=0, folder_uid=NULL WHERE org_id=? AND uid=?", orgID, dashboardUID)
|
_, err := sess.Exec("UPDATE dashboard SET deleted=NULL, folder_id=0, folder_uid=NULL WHERE org_id=? AND uid=?", orgID, dashboardUID)
|
||||||
@@ -549,6 +588,9 @@ func (d *dashboardStore) RestoreDashboard(ctx context.Context, orgID int64, dash
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUID string) error {
|
func (d *dashboardStore) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUID string) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.SoftDeleteDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
_, err := sess.Exec("UPDATE dashboard SET deleted=? WHERE org_id=? AND uid=?", time.Now(), orgID, dashboardUID)
|
_, err := sess.Exec("UPDATE dashboard SET deleted=? WHERE org_id=? AND uid=?", time.Now(), orgID, dashboardUID)
|
||||||
return err
|
return err
|
||||||
@@ -556,6 +598,9 @@ func (d *dashboardStore) SoftDeleteDashboard(ctx context.Context, orgID int64, d
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) SoftDeleteDashboardsInFolders(ctx context.Context, orgID int64, folderUids []string) error {
|
func (d *dashboardStore) SoftDeleteDashboardsInFolders(ctx context.Context, orgID int64, folderUids []string) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.SoftDeleteDashboardsInFolders")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if len(folderUids) == 0 {
|
if len(folderUids) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -580,6 +625,9 @@ func (d *dashboardStore) SoftDeleteDashboardsInFolders(ctx context.Context, orgI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) DeleteDashboard(ctx context.Context, cmd *dashboards.DeleteDashboardCommand) error {
|
func (d *dashboardStore) DeleteDashboard(ctx context.Context, cmd *dashboards.DeleteDashboardCommand) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.DeleteDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
return d.deleteDashboard(cmd, sess, d.emitEntityEvent())
|
return d.deleteDashboard(cmd, sess, d.emitEntityEvent())
|
||||||
})
|
})
|
||||||
@@ -736,6 +784,9 @@ func createEntityEvent(dashboard *dashboards.Dashboard, eventType store.EntityEv
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) {
|
func (d *dashboardStore) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var queryResult *dashboards.Dashboard
|
var queryResult *dashboards.Dashboard
|
||||||
err := d.store.ReadReplica().WithDbSession(ctx, func(sess *db.Session) error {
|
err := d.store.ReadReplica().WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
||||||
@@ -778,6 +829,9 @@ func (d *dashboardStore) GetDashboard(ctx context.Context, query *dashboards.Get
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) {
|
func (d *dashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboardUIDByID")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
us := &dashboards.DashboardRef{}
|
us := &dashboards.DashboardRef{}
|
||||||
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
var rawSQL = `SELECT uid, slug from dashboard WHERE Id=?`
|
var rawSQL = `SELECT uid, slug from dashboard WHERE Id=?`
|
||||||
@@ -796,6 +850,9 @@ func (d *dashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) {
|
func (d *dashboardStore) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var dashboards = make([]*dashboards.Dashboard, 0)
|
var dashboards = make([]*dashboards.Dashboard, 0)
|
||||||
err := d.store.ReadReplica().WithDbSession(ctx, func(sess *db.Session) error {
|
err := d.store.ReadReplica().WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 {
|
if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 {
|
||||||
@@ -824,6 +881,9 @@ func (d *dashboardStore) GetDashboards(ctx context.Context, query *dashboards.Ge
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.FindDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
recursiveQueriesAreSupported, err := d.store.ReadReplica().RecursiveQueriesAreSupported()
|
recursiveQueriesAreSupported, err := d.store.ReadReplica().RecursiveQueriesAreSupported()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -914,6 +974,9 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) {
|
func (d *dashboardStore) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboardTags")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
queryResult := make([]*dashboards.DashboardTagCloudItem, 0)
|
queryResult := make([]*dashboards.DashboardTagCloudItem, 0)
|
||||||
err := d.store.DB().WithDbSession(ctx, func(dbSession *db.Session) error {
|
err := d.store.DB().WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||||
sql := `SELECT
|
sql := `SELECT
|
||||||
@@ -939,6 +1002,9 @@ func (d *dashboardStore) GetDashboardTags(ctx context.Context, query *dashboards
|
|||||||
// given parent folder ID.
|
// given parent folder ID.
|
||||||
func (d *dashboardStore) CountDashboardsInFolders(
|
func (d *dashboardStore) CountDashboardsInFolders(
|
||||||
ctx context.Context, req *dashboards.CountDashboardsInFolderRequest) (int64, error) {
|
ctx context.Context, req *dashboards.CountDashboardsInFolderRequest) (int64, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.CountDashboardsInFolders")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if len(req.FolderUIDs) == 0 {
|
if len(req.FolderUIDs) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
@@ -967,6 +1033,9 @@ func (d *dashboardStore) CountDashboardsInFolders(
|
|||||||
|
|
||||||
func (d *dashboardStore) DeleteDashboardsInFolders(
|
func (d *dashboardStore) DeleteDashboardsInFolders(
|
||||||
ctx context.Context, req *dashboards.DeleteDashboardsInFolderRequest) error {
|
ctx context.Context, req *dashboards.DeleteDashboardsInFolderRequest) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.DeleteDashboardsInFolders")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
return d.store.DB().WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||||
// TODO delete all dashboards in the folder in a bulk query
|
// TODO delete all dashboards in the folder in a bulk query
|
||||||
for _, folderUID := range req.FolderUIDs {
|
for _, folderUID := range req.FolderUIDs {
|
||||||
@@ -993,6 +1062,9 @@ func (d *dashboardStore) DeleteDashboardsInFolders(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetAllDashboards(ctx context.Context) ([]*dashboards.Dashboard, error) {
|
func (d *dashboardStore) GetAllDashboards(ctx context.Context) ([]*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetAllDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var dashboards = make([]*dashboards.Dashboard, 0)
|
var dashboards = make([]*dashboards.Dashboard, 0)
|
||||||
err := d.store.ReadReplica().WithDbSession(ctx, func(session *db.Session) error {
|
err := d.store.ReadReplica().WithDbSession(ctx, func(session *db.Session) error {
|
||||||
err := session.Find(&dashboards)
|
err := session.Find(&dashboards)
|
||||||
@@ -1005,6 +1077,9 @@ func (d *dashboardStore) GetAllDashboards(ctx context.Context) ([]*dashboards.Da
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dashboardStore) GetSoftDeletedExpiredDashboards(ctx context.Context, duration time.Duration) ([]*dashboards.Dashboard, error) {
|
func (d *dashboardStore) GetSoftDeletedExpiredDashboards(ctx context.Context, duration time.Duration) ([]*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.database.GetSoftDeletedExpiredDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var dashboards = make([]*dashboards.Dashboard, 0)
|
var dashboards = make([]*dashboards.Dashboard, 0)
|
||||||
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
err := d.store.DB().WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
err := sess.Where("deleted IS NOT NULL AND deleted < ?", time.Now().Add(-duration)).Find(&dashboards)
|
err := sess.Where("deleted IS NOT NULL AND deleted < ?", time.Now().Add(-duration)).Find(&dashboards)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/grafana/authlib/claims"
|
"github.com/grafana/authlib/claims"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
@@ -43,6 +44,7 @@ var (
|
|||||||
_ dashboards.PluginService = (*DashboardServiceImpl)(nil)
|
_ dashboards.PluginService = (*DashboardServiceImpl)(nil)
|
||||||
|
|
||||||
daysInTrash = 24 * 30 * time.Hour
|
daysInTrash = 24 * 30 * time.Hour
|
||||||
|
tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/dashboards/service")
|
||||||
)
|
)
|
||||||
|
|
||||||
type DashboardServiceImpl struct {
|
type DashboardServiceImpl struct {
|
||||||
@@ -103,6 +105,9 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co
|
|||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
||||||
validateProvisionedDashboard bool) (*dashboards.SaveDashboardCommand, error) {
|
validateProvisionedDashboard bool) (*dashboards.SaveDashboardCommand, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.BuildSaveDashboardcommand")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
dash := dto.Dashboard
|
dash := dto.Dashboard
|
||||||
|
|
||||||
dash.OrgID = dto.OrgID
|
dash.OrgID = dto.OrgID
|
||||||
@@ -242,6 +247,9 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.
|
|||||||
// getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard
|
// getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard
|
||||||
// It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck()
|
// It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck()
|
||||||
func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user identity.Requester) (guardian.DashboardGuardian, error) {
|
func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user identity.Requester) (guardian.DashboardGuardian, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.getGuardianForSavePermissionCheck")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
newDashboard := d.ID == 0
|
newDashboard := d.ID == 0
|
||||||
|
|
||||||
if newDashboard {
|
if newDashboard {
|
||||||
@@ -290,6 +298,9 @@ func validateDashboardRefreshInterval(minRefreshInterval string, dash *dashboard
|
|||||||
|
|
||||||
func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
||||||
provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) {
|
provisioning *dashboards.DashboardProvisioning) (*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.SaveProvisionedDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
||||||
dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid",
|
dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid",
|
||||||
dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", dr.cfg.MinRefreshInterval)
|
dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", dr.cfg.MinRefreshInterval)
|
||||||
@@ -317,6 +328,9 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *folder.CreateFolderCommand) (*folder.Folder, error) {
|
func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.Context, dto *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.SaveFolderForProvisionedDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
dto.SignedInUser = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions)
|
dto.SignedInUser = accesscontrol.BackgroundUser("dashboard_provisioning", dto.OrgID, org.RoleAdmin, provisionerPermissions)
|
||||||
f, err := dr.folderService.Create(ctx, dto)
|
f, err := dr.folderService.Create(ctx, dto)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -330,6 +344,9 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C
|
|||||||
|
|
||||||
func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
||||||
allowUiUpdate bool) (*dashboards.Dashboard, error) {
|
allowUiUpdate bool) (*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.SaveDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
||||||
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
|
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
|
||||||
"dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval",
|
"dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval",
|
||||||
@@ -359,6 +376,9 @@ func (dr *DashboardServiceImpl) GetSoftDeletedDashboard(ctx context.Context, org
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) RestoreDashboard(ctx context.Context, dashboard *dashboards.Dashboard, user identity.Requester, optionalFolderUID string) error {
|
func (dr *DashboardServiceImpl) RestoreDashboard(ctx context.Context, dashboard *dashboards.Dashboard, user identity.Requester, optionalFolderUID string) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.RestoreDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if !dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
if !dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
||||||
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
|
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
|
||||||
}
|
}
|
||||||
@@ -398,6 +418,9 @@ func (dr *DashboardServiceImpl) RestoreDashboard(ctx context.Context, dashboard
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUID string) error {
|
func (dr *DashboardServiceImpl) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUID string) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.SoftDeleteDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if !dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
if !dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
||||||
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
|
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
|
||||||
}
|
}
|
||||||
@@ -426,6 +449,9 @@ func (dr *DashboardServiceImpl) DeleteProvisionedDashboard(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId int64, orgId int64, validateProvisionedDashboard bool) error {
|
func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId int64, orgId int64, validateProvisionedDashboard bool) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.deleteDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if validateProvisionedDashboard {
|
if validateProvisionedDashboard {
|
||||||
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dashboardId)
|
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dashboardId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -442,6 +468,9 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId
|
|||||||
|
|
||||||
func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) (
|
func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) (
|
||||||
*dashboards.Dashboard, error) {
|
*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.ImportDashboard")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
||||||
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
|
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
|
||||||
"dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title,
|
"dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title,
|
||||||
@@ -475,6 +504,9 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) {
|
func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultPermissions")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
resource := "dashboard"
|
resource := "dashboard"
|
||||||
if dash.IsFolder {
|
if dash.IsFolder {
|
||||||
resource = "folder"
|
resource = "folder"
|
||||||
@@ -518,6 +550,9 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, cmd *folder.CreateFolderCommand, f *folder.Folder, provisioned bool) {
|
func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, cmd *folder.CreateFolderCommand, f *folder.Folder, provisioned bool) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultFolderPermissions")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if !dr.cfg.RBAC.PermissionsOnCreation("folder") {
|
if !dr.cfg.RBAC.PermissionsOnCreation("folder") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -565,6 +600,9 @@ func (dr *DashboardServiceImpl) GetDashboardsSharedWithUser(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*dashboards.Dashboard, error) {
|
func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.getDashboardsSharedWithUser")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
permissions := user.GetPermissions()
|
permissions := user.GetPermissions()
|
||||||
dashboardPermissions := permissions[dashboards.ActionDashboardsRead]
|
dashboardPermissions := permissions[dashboards.ActionDashboardsRead]
|
||||||
sharedDashboards := make([]*dashboards.Dashboard, 0)
|
sharedDashboards := make([]*dashboards.Dashboard, 0)
|
||||||
@@ -594,6 +632,9 @@ func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context,
|
|||||||
|
|
||||||
// filterUserSharedDashboards filter dashboards directly assigned to user, but not located in folders with view permissions
|
// filterUserSharedDashboards filter dashboards directly assigned to user, but not located in folders with view permissions
|
||||||
func (dr *DashboardServiceImpl) filterUserSharedDashboards(ctx context.Context, user identity.Requester, userDashboards []*dashboards.Dashboard) ([]*dashboards.Dashboard, error) {
|
func (dr *DashboardServiceImpl) filterUserSharedDashboards(ctx context.Context, user identity.Requester, userDashboards []*dashboards.Dashboard) ([]*dashboards.Dashboard, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.filterUserSharedDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
filteredDashboards := make([]*dashboards.Dashboard, 0)
|
filteredDashboards := make([]*dashboards.Dashboard, 0)
|
||||||
|
|
||||||
folderUIDs := make([]string, 0)
|
folderUIDs := make([]string, 0)
|
||||||
@@ -632,6 +673,9 @@ func (dr *DashboardServiceImpl) filterUserSharedDashboards(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) getUserSharedDashboardUIDs(ctx context.Context, user identity.Requester) ([]string, error) {
|
func (dr *DashboardServiceImpl) getUserSharedDashboardUIDs(ctx context.Context, user identity.Requester) ([]string, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.getUserSharedDashboardsUIDs")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
userDashboards, err := dr.getDashboardsSharedWithUser(ctx, user)
|
userDashboards, err := dr.getDashboardsSharedWithUser(ctx, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -644,6 +688,9 @@ func (dr *DashboardServiceImpl) getUserSharedDashboardUIDs(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.FindDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && len(query.FolderUIDs) > 0 && slices.Contains(query.FolderUIDs, folder.SharedWithMeFolderUID) {
|
if dr.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && len(query.FolderUIDs) > 0 && slices.Contains(query.FolderUIDs, folder.SharedWithMeFolderUID) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
userDashboardUIDs, err := dr.getUserSharedDashboardUIDs(ctx, query.SignedInUser)
|
userDashboardUIDs, err := dr.getUserSharedDashboardUIDs(ctx, query.SignedInUser)
|
||||||
@@ -665,6 +712,9 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (model.HitList, error) {
|
func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (model.HitList, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.SearchDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
res, err := dr.FindDashboards(ctx, query)
|
res, err := dr.FindDashboards(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -745,6 +795,9 @@ func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) error {
|
func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) error {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.DeleteInFolders")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
if dr.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
||||||
return dr.dashboardStore.SoftDeleteDashboardsInFolders(ctx, orgID, folderUIDs)
|
return dr.dashboardStore.SoftDeleteDashboardsInFolders(ctx, orgID, folderUIDs)
|
||||||
}
|
}
|
||||||
@@ -755,6 +808,9 @@ func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64
|
|||||||
func (dr *DashboardServiceImpl) Kind() string { return entity.StandardKindDashboard }
|
func (dr *DashboardServiceImpl) Kind() string { return entity.StandardKindDashboard }
|
||||||
|
|
||||||
func (dr *DashboardServiceImpl) CleanUpDeletedDashboards(ctx context.Context) (int64, error) {
|
func (dr *DashboardServiceImpl) CleanUpDeletedDashboards(ctx context.Context) (int64, error) {
|
||||||
|
ctx, span := tracer.Start(ctx, "dashboards.service.CleanUpDeletedDashboards")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
var deletedDashboardsCount int64
|
var deletedDashboardsCount int64
|
||||||
deletedDashboards, err := dr.dashboardStore.GetSoftDeletedExpiredDashboards(ctx, daysInTrash)
|
deletedDashboards, err := dr.dashboardStore.GetSoftDeletedExpiredDashboards(ctx, daysInTrash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1397,6 +1397,34 @@ var (
|
|||||||
Stage: FeatureStageExperimental,
|
Stage: FeatureStageExperimental,
|
||||||
Owner: grafanaObservabilityLogsSquad,
|
Owner: grafanaObservabilityLogsSquad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "singleTopNav",
|
||||||
|
Description: "Unifies the top search bar and breadcrumb bar into one",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Owner: grafanaFrontendPlatformSquad,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "exploreLogsShardSplitting",
|
||||||
|
Description: "Used in Explore Logs to split queries into multiple queries based on the number of shards",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Owner: grafanaObservabilityLogsSquad,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "exploreLogsAggregatedMetrics",
|
||||||
|
Description: "Used in Explore Logs to query by aggregated metrics",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Owner: grafanaObservabilityLogsSquad,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "exploreLogsLimitedTimeRange",
|
||||||
|
Description: "Used in Explore Logs to limit the time range",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Owner: grafanaObservabilityLogsSquad,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -184,3 +184,7 @@ backgroundPluginInstaller,experimental,@grafana/plugins-platform-backend,false,t
|
|||||||
dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||||
adhocFilterOneOf,experimental,@grafana/dashboards-squad,false,false,false
|
adhocFilterOneOf,experimental,@grafana/dashboards-squad,false,false,false
|
||||||
lokiSendDashboardPanelNames,experimental,@grafana/observability-logs,false,false,false
|
lokiSendDashboardPanelNames,experimental,@grafana/observability-logs,false,false,false
|
||||||
|
singleTopNav,experimental,@grafana/grafana-frontend-platform,false,false,true
|
||||||
|
exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true
|
||||||
|
exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true
|
||||||
|
exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true
|
||||||
|
|||||||
|
@@ -746,4 +746,20 @@ const (
|
|||||||
// FlagLokiSendDashboardPanelNames
|
// FlagLokiSendDashboardPanelNames
|
||||||
// Send dashboard and panel names to Loki when querying
|
// Send dashboard and panel names to Loki when querying
|
||||||
FlagLokiSendDashboardPanelNames = "lokiSendDashboardPanelNames"
|
FlagLokiSendDashboardPanelNames = "lokiSendDashboardPanelNames"
|
||||||
|
|
||||||
|
// FlagSingleTopNav
|
||||||
|
// Unifies the top search bar and breadcrumb bar into one
|
||||||
|
FlagSingleTopNav = "singleTopNav"
|
||||||
|
|
||||||
|
// FlagExploreLogsShardSplitting
|
||||||
|
// Used in Explore Logs to split queries into multiple queries based on the number of shards
|
||||||
|
FlagExploreLogsShardSplitting = "exploreLogsShardSplitting"
|
||||||
|
|
||||||
|
// FlagExploreLogsAggregatedMetrics
|
||||||
|
// Used in Explore Logs to query by aggregated metrics
|
||||||
|
FlagExploreLogsAggregatedMetrics = "exploreLogsAggregatedMetrics"
|
||||||
|
|
||||||
|
// FlagExploreLogsLimitedTimeRange
|
||||||
|
// Used in Explore Logs to limit the time range
|
||||||
|
FlagExploreLogsLimitedTimeRange = "exploreLogsLimitedTimeRange"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1046,6 +1046,45 @@
|
|||||||
"allowSelfServe": true
|
"allowSelfServe": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "exploreLogsAggregatedMetrics",
|
||||||
|
"resourceVersion": "1724938092041",
|
||||||
|
"creationTimestamp": "2024-08-29T13:28:12Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Used in Explore Logs to query by aggregated metrics",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/observability-logs",
|
||||||
|
"frontend": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "exploreLogsLimitedTimeRange",
|
||||||
|
"resourceVersion": "1724938092041",
|
||||||
|
"creationTimestamp": "2024-08-29T13:28:12Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Used in Explore Logs to limit the time range",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/observability-logs",
|
||||||
|
"frontend": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "exploreLogsShardSplitting",
|
||||||
|
"resourceVersion": "1724938092041",
|
||||||
|
"creationTimestamp": "2024-08-29T13:28:12Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Used in Explore Logs to split queries into multiple queries based on the number of shards",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/observability-logs",
|
||||||
|
"frontend": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "exploreMetrics",
|
"name": "exploreMetrics",
|
||||||
@@ -2501,6 +2540,19 @@
|
|||||||
"codeowner": "@grafana/dashboards-squad"
|
"codeowner": "@grafana/dashboards-squad"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "singleTopNav",
|
||||||
|
"resourceVersion": "1724861961030",
|
||||||
|
"creationTimestamp": "2024-08-28T16:19:21Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Unifies the top search bar and breadcrumb bar into one",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/grafana-frontend-platform",
|
||||||
|
"frontend": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "sqlDatasourceDatabaseSelection",
|
"name": "sqlDatasourceDatabaseSelection",
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ func TestRouteGetReceiversResponses(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("json body content is as expected", func(t *testing.T) {
|
t.Run("json body content is as expected", func(t *testing.T) {
|
||||||
expectedRedactedResponse := `{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthPassword":"[REDACTED]","basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","url":"[REDACTED]","use_discord_username":true},"secureFields":{"url":true}}]}`
|
expectedRedactedResponse := `{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","use_discord_username":true},"secureFields":{"url":true}}]}`
|
||||||
expectedDecryptedResponse := `{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthPassword":"testpass","basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","url":"some url","use_discord_username":true},"secureFields":{"url":true}}]}`
|
expectedDecryptedResponse := `{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthPassword":"testpass","basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","url":"some url","use_discord_username":true},"secureFields":{"url":true}}]}`
|
||||||
t.Run("decrypt false", func(t *testing.T) {
|
t.Run("decrypt false", func(t *testing.T) {
|
||||||
env := createTestEnv(t, testContactPointConfig)
|
env := createTestEnv(t, testContactPointConfig)
|
||||||
|
|||||||
@@ -32,13 +32,17 @@ func IntegrationToPostableGrafanaReceiver(integration *models.Integration) (*api
|
|||||||
SecureSettings: maps.Clone(integration.SecureSettings),
|
SecureSettings: maps.Clone(integration.SecureSettings),
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(integration.Settings) > 0 {
|
// Alertmanager will fail validation with nil Settings , so ensure we always have at least an empty map.
|
||||||
jsonBytes, err := json.Marshal(integration.Settings)
|
settings := integration.Settings
|
||||||
if err != nil {
|
if settings == nil {
|
||||||
return nil, err
|
settings = make(map[string]any)
|
||||||
}
|
|
||||||
postable.Settings = jsonBytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jsonBytes, err := json.Marshal(settings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
postable.Settings = jsonBytes
|
||||||
return postable, nil
|
return postable, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ var (
|
|||||||
ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one."))
|
ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one."))
|
||||||
|
|
||||||
ErrReceiverNotFound = errutil.NotFound("alerting.notifications.receivers.notFound", errutil.WithPublicMessage("Receiver not found"))
|
ErrReceiverNotFound = errutil.NotFound("alerting.notifications.receivers.notFound", errutil.WithPublicMessage("Receiver not found"))
|
||||||
ErrReceiverExists = errutil.BadRequest("alerting.notifications.receivers.exists", errutil.WithPublicMessage("Receiver with this name already exists. Use a different name or update an existing one."))
|
ErrReceiverExists = errutil.Conflict("alerting.notifications.receivers.exists", errutil.WithPublicMessage("Receiver with this name already exists. Use a different name or update an existing one."))
|
||||||
ErrReceiverInvalid = errutil.Conflict("alerting.notifications.receivers.invalid").MustTemplate(
|
ErrReceiverInvalid = errutil.BadRequest("alerting.notifications.receivers.invalid").MustTemplate(
|
||||||
"Invalid receiver: '{{ .Public.Reason }}'",
|
"Invalid receiver: '{{ .Public.Reason }}'",
|
||||||
errutil.WithPublic("Invalid receiver: '{{ .Public.Reason }}'"),
|
errutil.WithPublic("Invalid receiver: '{{ .Public.Reason }}'"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -130,7 +130,17 @@ func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiver
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rs.decryptOrRedactSecureSettings(ctx, rcv, q.Decrypt)
|
if q.Decrypt {
|
||||||
|
err := rcv.Decrypt(rs.decryptor(ctx))
|
||||||
|
if err != nil {
|
||||||
|
rs.log.Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := rcv.Encrypt(rs.encryptor(ctx))
|
||||||
|
if err != nil {
|
||||||
|
rs.log.Warn("Failed to encrypt secure settings", "name", rcv.Name, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rcv, nil
|
return rcv, nil
|
||||||
}
|
}
|
||||||
@@ -167,8 +177,18 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range filtered {
|
for _, rcv := range filtered {
|
||||||
rs.decryptOrRedactSecureSettings(ctx, r, q.Decrypt)
|
if q.Decrypt {
|
||||||
|
err := rcv.Decrypt(rs.decryptor(ctx))
|
||||||
|
if err != nil {
|
||||||
|
rs.log.Warn("Failed to decrypt secure settings", "name", rcv.Name, "error", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := rcv.Encrypt(rs.encryptor(ctx))
|
||||||
|
if err != nil {
|
||||||
|
rs.log.Warn("Failed to encrypt secure settings", "name", rcv.Name, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return limitOffset(filtered, q.Offset, q.Limit), nil
|
return limitOffset(filtered, q.Offset, q.Limit), nil
|
||||||
@@ -260,7 +280,7 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, calle
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rs.log.Debug("ignoring optimistic concurrency check because version was not provided", "receiver", existing.Name, "operation", "delete")
|
rs.log.Debug("Ignoring optimistic concurrency check because version was not provided", "receiver", existing.Name, "operation", "delete")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rs.provenanceValidator(existing.Provenance, models.Provenance(callerProvenance)); err != nil {
|
if err := rs.provenanceValidator(existing.Provenance, models.Provenance(callerProvenance)); err != nil {
|
||||||
@@ -460,17 +480,6 @@ func (rs *ReceiverService) deleteProvenances(ctx context.Context, orgID int64, i
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *ReceiverService) decryptOrRedactSecureSettings(ctx context.Context, recv *models.Receiver, decrypt bool) {
|
|
||||||
if decrypt {
|
|
||||||
err := recv.Decrypt(rs.decryptor(ctx))
|
|
||||||
if err != nil {
|
|
||||||
rs.log.Warn("failed to decrypt secure settings", "name", recv.Name, "error", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
recv.Redact(rs.redactor())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decryptor returns a models.DecryptFn that decrypts a secure setting. If decryption fails, the fallback value is used.
|
// decryptor returns a models.DecryptFn that decrypts a secure setting. If decryption fails, the fallback value is used.
|
||||||
func (rs *ReceiverService) decryptor(ctx context.Context) models.DecryptFn {
|
func (rs *ReceiverService) decryptor(ctx context.Context) models.DecryptFn {
|
||||||
return func(value string) (string, error) {
|
return func(value string) (string, error) {
|
||||||
@@ -486,13 +495,6 @@ func (rs *ReceiverService) decryptor(ctx context.Context) models.DecryptFn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// redactor returns a models.RedactFn that redacts a secure setting.
|
|
||||||
func (rs *ReceiverService) redactor() models.RedactFn {
|
|
||||||
return func(value string) string {
|
|
||||||
return definitions.RedactedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// encryptor creates an encrypt function that delegates to secrets.Service and returns the base64 encoded result.
|
// encryptor creates an encrypt function that delegates to secrets.Service and returns the base64 encoded result.
|
||||||
func (rs *ReceiverService) encryptor(ctx context.Context) models.EncryptFn {
|
func (rs *ReceiverService) encryptor(ctx context.Context) models.EncryptFn {
|
||||||
return func(payload string) (string, error) {
|
return func(payload string) (string, error) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/alertmanager/config"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@@ -178,8 +179,14 @@ func TestReceiverService_DecryptRedact(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
if tc.decrypt {
|
if tc.decrypt {
|
||||||
require.Equal(t, "secure url", res.Integrations[0].Settings["url"])
|
require.Equal(t, "secure url", res.Integrations[0].Settings["url"])
|
||||||
|
require.NotContains(t, res.Integrations[0].SecureSettings, "url")
|
||||||
} else {
|
} else {
|
||||||
require.Equal(t, definitions.RedactedValue, res.Integrations[0].Settings["url"])
|
require.NotContains(t, res.Integrations[0].Settings, "url")
|
||||||
|
|
||||||
|
// Ensure the encrypted value exists and is not redacted or decrypted.
|
||||||
|
require.NotEmpty(t, res.Integrations[0].SecureSettings["url"])
|
||||||
|
require.NotEqual(t, definitions.RedactedValue, res.Integrations[0].SecureSettings["url"])
|
||||||
|
require.NotEqual(t, "secure url", res.Integrations[0].SecureSettings["url"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -248,7 +255,7 @@ func TestReceiverService_Delete(t *testing.T) {
|
|||||||
deleteUID: baseReceiver.UID,
|
deleteUID: baseReceiver.UID,
|
||||||
existing: util.Pointer(baseReceiver.Clone()),
|
existing: util.Pointer(baseReceiver.Clone()),
|
||||||
storeSettings: map[models.AlertRuleKey][]models.NotificationSettings{
|
storeSettings: map[models.AlertRuleKey][]models.NotificationSettings{
|
||||||
models.AlertRuleKey{OrgID: 1, UID: "rule1"}: {
|
{OrgID: 1, UID: "rule1"}: {
|
||||||
models.NotificationSettingsGen(models.NSMuts.WithReceiver(baseReceiver.Name))(),
|
models.NotificationSettingsGen(models.NSMuts.WithReceiver(baseReceiver.Name))(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -342,6 +349,7 @@ func TestReceiverService_Create(t *testing.T) {
|
|||||||
|
|
||||||
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))()
|
slackIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("slack"))()
|
||||||
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))()
|
emailIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("email"))()
|
||||||
|
lineIntegration := models.IntegrationGen(models.IntegrationMuts.WithName("test receiver"), models.IntegrationMuts.WithValidConfig("line"))()
|
||||||
baseReceiver := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver"), models.ReceiverMuts.WithIntegrations(slackIntegration))()
|
baseReceiver := models.ReceiverGen(models.ReceiverMuts.WithName("test receiver"), models.ReceiverMuts.WithIntegrations(slackIntegration))()
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -349,6 +357,7 @@ func TestReceiverService_Create(t *testing.T) {
|
|||||||
user identity.Requester
|
user identity.Requester
|
||||||
receiver models.Receiver
|
receiver models.Receiver
|
||||||
expectedCreate models.Receiver
|
expectedCreate models.Receiver
|
||||||
|
expectedStored *definitions.PostableApiReceiver
|
||||||
expectedErr error
|
expectedErr error
|
||||||
expectedProvenances map[string]models.Provenance
|
expectedProvenances map[string]models.Provenance
|
||||||
}{
|
}{
|
||||||
@@ -414,6 +423,49 @@ func TestReceiverService_Create(t *testing.T) {
|
|||||||
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithInvalidIntegration("slack")),
|
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithInvalidIntegration("slack")),
|
||||||
expectedErr: legacy_storage.ErrReceiverInvalid,
|
expectedErr: legacy_storage.ErrReceiverInvalid,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "create integration with no normal settings should not store nil settings",
|
||||||
|
user: writer,
|
||||||
|
receiver: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(
|
||||||
|
models.CopyIntegrationWith(lineIntegration,
|
||||||
|
models.IntegrationMuts.WithSettings(
|
||||||
|
map[string]any{ // Line is valid with only the single secure field "token", so Settings will be empty when saving.
|
||||||
|
"token": "secret",
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
)),
|
||||||
|
expectedCreate: models.CopyReceiverWith(baseReceiver, models.ReceiverMuts.WithIntegrations(
|
||||||
|
models.CopyIntegrationWith(lineIntegration,
|
||||||
|
models.IntegrationMuts.WithSettings(
|
||||||
|
map[string]any{}, // Empty settings, not nil.
|
||||||
|
),
|
||||||
|
models.IntegrationMuts.WithSecureSettings(
|
||||||
|
map[string]string{
|
||||||
|
"token": "c2VjcmV0", // base64 encoded "secret".
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
expectedStored: &definitions.PostableApiReceiver{
|
||||||
|
Receiver: config.Receiver{
|
||||||
|
Name: lineIntegration.Name,
|
||||||
|
},
|
||||||
|
PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
|
||||||
|
GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
|
||||||
|
{
|
||||||
|
UID: lineIntegration.UID,
|
||||||
|
Name: lineIntegration.Name,
|
||||||
|
Type: lineIntegration.Config.Type,
|
||||||
|
DisableResolveMessage: lineIntegration.DisableResolveMessage,
|
||||||
|
Settings: definitions.RawMessage(`{}`), // Empty settings, not nil.
|
||||||
|
SecureSettings: map[string]string{
|
||||||
|
"token": "c2VjcmV0", // base64 encoded "secret".
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
sut := createReceiverServiceSut(t, &secretsService)
|
sut := createReceiverServiceSut(t, &secretsService)
|
||||||
@@ -462,9 +514,19 @@ func TestReceiverService_Create(t *testing.T) {
|
|||||||
decrypted.Version = tc.expectedCreate.Version // Version is calculated before decryption.
|
decrypted.Version = tc.expectedCreate.Version // Version is calculated before decryption.
|
||||||
assert.Equal(t, decrypted, *stored)
|
assert.Equal(t, decrypted, *stored)
|
||||||
|
|
||||||
provenances, err := sut.provisioningStore.GetProvenances(context.Background(), tc.user.GetOrgID(), (&definitions.EmbeddedContactPoint{}).ResourceType())
|
if tc.expectedProvenances != nil {
|
||||||
require.NoError(t, err)
|
provenances, err := sut.provisioningStore.GetProvenances(context.Background(), tc.user.GetOrgID(), (&definitions.EmbeddedContactPoint{}).ResourceType())
|
||||||
assert.Equal(t, tc.expectedProvenances, provenances)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectedProvenances, provenances)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectedStored != nil {
|
||||||
|
revision, err := sut.cfgStore.Get(context.Background(), writer.GetOrgID())
|
||||||
|
require.NoError(t, err)
|
||||||
|
rcv, err := revision.GetReceiver(legacy_storage.NameToUid(tc.expectedStored.Name))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectedStored, rcv)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ func (ecp *ContactPointService) GetContactPoints(ctx context.Context, q ContactP
|
|||||||
contactPoints := make([]apimodels.EmbeddedContactPoint, 0, len(res))
|
contactPoints := make([]apimodels.EmbeddedContactPoint, 0, len(res))
|
||||||
for _, recv := range res {
|
for _, recv := range res {
|
||||||
for _, gr := range recv.Integrations {
|
for _, gr := range recv.Integrations {
|
||||||
|
if !q.Decrypt {
|
||||||
|
// Provisioning API redacts by default.
|
||||||
|
gr.Redact(func(value string) string {
|
||||||
|
return apimodels.RedactedValue
|
||||||
|
})
|
||||||
|
}
|
||||||
contactPoints = append(contactPoints, GrafanaIntegrationConfigToEmbeddedContactPoint(gr, recv.Provenance))
|
contactPoints = append(contactPoints, GrafanaIntegrationConfigToEmbeddedContactPoint(gr, recv.Provenance))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@@ -107,6 +108,7 @@ func (s *Service) installPlugins(ctx context.Context) error {
|
|||||||
|
|
||||||
s.log.Info("Installing plugin", "pluginId", installPlugin.ID, "version", installPlugin.Version)
|
s.log.Info("Installing plugin", "pluginId", installPlugin.ID, "version", installPlugin.Version)
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
ctx = repo.WithRequestOrigin(ctx, "preinstall")
|
||||||
err := s.pluginInstaller.Add(ctx, installPlugin.ID, installPlugin.Version, compatOpts)
|
err := s.pluginInstaller.Add(ctx, installPlugin.ID, installPlugin.Version, compatOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var dupeErr plugins.DuplicateError
|
var dupeErr plugins.DuplicateError
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import (
|
|||||||
var (
|
var (
|
||||||
// ErrFolderNameMissing is returned when folder name is missing.
|
// ErrFolderNameMissing is returned when folder name is missing.
|
||||||
ErrFolderNameMissing = errors.New("folder name missing")
|
ErrFolderNameMissing = errors.New("folder name missing")
|
||||||
|
// ErrGetOrCreateFolder is returned when there is a failure to fetch or create a provisioning folder.
|
||||||
|
ErrGetOrCreateFolder = errors.New("failed to get or create provisioning folder")
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileReader is responsible for reading dashboards from disk and
|
// FileReader is responsible for reading dashboards from disk and
|
||||||
@@ -147,7 +149,7 @@ func (fr *FileReader) storeDashboardsInFolder(ctx context.Context, filesFoundOnD
|
|||||||
dashboardRefs map[string]*dashboards.DashboardProvisioning, usageTracker *usageTracker) error {
|
dashboardRefs map[string]*dashboards.DashboardProvisioning, usageTracker *usageTracker) error {
|
||||||
folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder)
|
folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder)
|
||||||
if err != nil && !errors.Is(err, ErrFolderNameMissing) {
|
if err != nil && !errors.Is(err, ErrFolderNameMissing) {
|
||||||
return err
|
return fmt.Errorf("%w with name %q: %w", ErrGetOrCreateFolder, fr.Cfg.Folder, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// save dashboards based on json files
|
// save dashboards based on json files
|
||||||
@@ -177,7 +179,7 @@ func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(ctx context.Cont
|
|||||||
|
|
||||||
folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, folderName)
|
folderID, folderUID, err := fr.getOrCreateFolder(ctx, fr.Cfg, fr.dashboardProvisioningService, folderName)
|
||||||
if err != nil && !errors.Is(err, ErrFolderNameMissing) {
|
if err != nil && !errors.Is(err, ErrFolderNameMissing) {
|
||||||
return fmt.Errorf("can't provision folder %q from file system structure: %w", folderName, err)
|
return fmt.Errorf("%w with name %q from file system structure: %w", ErrGetOrCreateFolder, folderName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
provisioningMetadata, err := fr.saveDashboard(ctx, path, folderID, folderUID, fileInfo, dashboardRefs)
|
provisioningMetadata, err := fr.saveDashboard(ctx, path, folderID, folderUID, fileInfo, dashboardRefs)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package provisioning
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -77,9 +78,8 @@ func ProvideService(
|
|||||||
folderService: folderService,
|
folderService: folderService,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.setDashboardProvisioner()
|
if err := s.setDashboardProvisioner(); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, fmt.Errorf("%v: %w", "Failed to create provisioner", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
@@ -106,30 +106,27 @@ type ProvisioningService interface {
|
|||||||
GetAllowUIUpdatesFromConfig(name string) bool
|
GetAllowUIUpdatesFromConfig(name string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a public constructor for overriding service to be able to instantiate OSS as fallback
|
|
||||||
func NewProvisioningServiceImpl() *ProvisioningServiceImpl {
|
|
||||||
logger := log.New("provisioning")
|
|
||||||
return &ProvisioningServiceImpl{
|
|
||||||
log: logger,
|
|
||||||
newDashboardProvisioner: dashboards.New,
|
|
||||||
provisionDatasources: datasources.Provision,
|
|
||||||
provisionPlugins: plugins.Provision,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for testing purposes
|
// Used for testing purposes
|
||||||
func newProvisioningServiceImpl(
|
func newProvisioningServiceImpl(
|
||||||
newDashboardProvisioner dashboards.DashboardProvisionerFactory,
|
newDashboardProvisioner dashboards.DashboardProvisionerFactory,
|
||||||
provisionDatasources func(context.Context, string, datasources.BaseDataSourceService, datasources.CorrelationsStore, org.Service) error,
|
provisionDatasources func(context.Context, string, datasources.BaseDataSourceService, datasources.CorrelationsStore, org.Service) error,
|
||||||
provisionPlugins func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error,
|
provisionPlugins func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error,
|
||||||
) *ProvisioningServiceImpl {
|
searchService searchV2.SearchService,
|
||||||
return &ProvisioningServiceImpl{
|
) (*ProvisioningServiceImpl, error) {
|
||||||
|
s := &ProvisioningServiceImpl{
|
||||||
log: log.New("provisioning"),
|
log: log.New("provisioning"),
|
||||||
newDashboardProvisioner: newDashboardProvisioner,
|
newDashboardProvisioner: newDashboardProvisioner,
|
||||||
provisionDatasources: provisionDatasources,
|
provisionDatasources: provisionDatasources,
|
||||||
provisionPlugins: provisionPlugins,
|
provisionPlugins: provisionPlugins,
|
||||||
Cfg: setting.NewCfg(),
|
Cfg: setting.NewCfg(),
|
||||||
|
searchService: searchService,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.setDashboardProvisioner(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProvisioningServiceImpl struct {
|
type ProvisioningServiceImpl struct {
|
||||||
@@ -185,7 +182,11 @@ func (ps *ProvisioningServiceImpl) Run(ctx context.Context) error {
|
|||||||
err := ps.ProvisionDashboards(ctx)
|
err := ps.ProvisionDashboards(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ps.log.Error("Failed to provision dashboard", "error", err)
|
ps.log.Error("Failed to provision dashboard", "error", err)
|
||||||
return err
|
// Consider the allow list of errors for which running the provisioning service should not
|
||||||
|
// fail. For now this includes only dashboards.ErrGetOrCreateFolder.
|
||||||
|
if !errors.Is(err, dashboards.ErrGetOrCreateFolder) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ps.dashboardProvisioner.HasDashboardSources() {
|
if ps.dashboardProvisioner.HasDashboardSources() {
|
||||||
ps.searchService.TriggerReIndex()
|
ps.searchService.TriggerReIndex()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user