Merge remote-tracking branch 'origin' into ivana/swagger-query-history
This commit is contained in:
@@ -146,12 +146,6 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/features/org/OrgDetailsPage.test.tsx:3835042085": [
|
||||
[0, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/org/OrgProfile.test.tsx:623809345": [
|
||||
[0, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/teams/TeamSettings.test.tsx:2043271249": [
|
||||
[0, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/features/users/UsersListPage.test.tsx:2518052139": [
|
||||
[0, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
|
||||
+29
-29
@@ -1505,7 +1505,7 @@ type: docker
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: oss-build-e2e-publish-release
|
||||
name: release-oss-build-e2e-publish
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -1802,7 +1802,7 @@ volumes:
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: oss-test-release
|
||||
name: release-oss-test
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -1920,7 +1920,7 @@ volumes:
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: oss-integration-tests-release
|
||||
name: release-oss-integration-tests
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -2010,11 +2010,11 @@ volumes:
|
||||
medium: memory
|
||||
---
|
||||
depends_on:
|
||||
- oss-build-e2e-publish-release
|
||||
- oss-test-release
|
||||
- oss-integration-tests-release
|
||||
- release-oss-build-e2e-publish
|
||||
- release-oss-test
|
||||
- release-oss-integration-tests
|
||||
kind: pipeline
|
||||
name: oss-windows-release
|
||||
name: release-oss-windows
|
||||
platform:
|
||||
arch: amd64
|
||||
os: windows
|
||||
@@ -2076,7 +2076,7 @@ depends_on: []
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
name: enterprise-build-e2e-publish-release
|
||||
name: release-enterprise-build-e2e-publish
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -2424,7 +2424,7 @@ depends_on: []
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
name: enterprise-test-release
|
||||
name: release-enterprise-test
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -2587,7 +2587,7 @@ depends_on: []
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
name: enterprise-integration-tests-release
|
||||
name: release-enterprise-integration-tests
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -2728,13 +2728,13 @@ volumes:
|
||||
clone:
|
||||
disable: true
|
||||
depends_on:
|
||||
- enterprise-build-e2e-publish-release
|
||||
- enterprise-test-release
|
||||
- enterprise-integration-tests-release
|
||||
- release-enterprise-build-e2e-publish
|
||||
- release-enterprise-test
|
||||
- release-enterprise-integration-tests
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
name: enterprise-windows-release
|
||||
name: release-enterprise-windows
|
||||
platform:
|
||||
arch: amd64
|
||||
os: windows
|
||||
@@ -3317,7 +3317,7 @@ volumes:
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: oss-build-e2e-publish-release-branch
|
||||
name: release-branch-oss-build-e2e-publish
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -3584,7 +3584,7 @@ volumes:
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: oss-test-release-branch
|
||||
name: release-branch-oss-test
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -3696,7 +3696,7 @@ volumes:
|
||||
---
|
||||
depends_on: []
|
||||
kind: pipeline
|
||||
name: oss-integration-tests-release-branch
|
||||
name: release-branch-oss-integration-tests
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -3780,11 +3780,11 @@ volumes:
|
||||
medium: memory
|
||||
---
|
||||
depends_on:
|
||||
- oss-build-e2e-publish-release-branch
|
||||
- oss-test-release-branch
|
||||
- oss-integration-tests-release-branch
|
||||
- release-branch-oss-build-e2e-publish
|
||||
- release-branch-oss-test
|
||||
- release-branch-oss-integration-tests
|
||||
kind: pipeline
|
||||
name: oss-windows-release-branch
|
||||
name: release-branch-oss-windows
|
||||
platform:
|
||||
arch: amd64
|
||||
os: windows
|
||||
@@ -3835,7 +3835,7 @@ depends_on: []
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
name: enterprise-build-e2e-publish-release-branch
|
||||
name: release-branch-enterprise-build-e2e-publish
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -4181,7 +4181,7 @@ depends_on: []
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
name: enterprise-test-release-branch
|
||||
name: release-branch-enterprise-test
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -4335,7 +4335,7 @@ depends_on: []
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
name: enterprise-integration-tests-release-branch
|
||||
name: release-branch-enterprise-integration-tests
|
||||
node:
|
||||
type: no-parallel
|
||||
platform:
|
||||
@@ -4467,13 +4467,13 @@ volumes:
|
||||
clone:
|
||||
disable: true
|
||||
depends_on:
|
||||
- enterprise-build-e2e-publish-release-branch
|
||||
- enterprise-test-release-branch
|
||||
- enterprise-integration-tests-release-branch
|
||||
- release-branch-enterprise-build-e2e-publish
|
||||
- release-branch-enterprise-test
|
||||
- release-branch-enterprise-integration-tests
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
name: enterprise-windows-release-branch
|
||||
name: release-branch-enterprise-windows
|
||||
platform:
|
||||
arch: amd64
|
||||
os: windows
|
||||
@@ -4683,6 +4683,6 @@ kind: secret
|
||||
name: gcp_upload_artifacts_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: 378147a306d077b0566f5353c9e5c236c40dbb8d81392091a1346ed275b86496
|
||||
hmac: 0abf901971b403a4cf5887d1051ec4a656335a23d9a92809a71312d115b4862f
|
||||
|
||||
...
|
||||
|
||||
@@ -8,7 +8,7 @@ weight: 114
|
||||
|
||||
# Grafana Alerting
|
||||
|
||||
Grafana alerts allow you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to:
|
||||
Grafana Alerting allows you to learn about problems in your systems moments after they occur. Robust and actionable alerts help you identify and resolve issues quickly, minimizing disruption to your services. It centralizes alerting information in a single, searchable view that allows you to:
|
||||
|
||||
- Create and manage Grafana alerts
|
||||
- Create and manage Grafana Mimir and Loki managed alerts
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
aliases:
|
||||
description:
|
||||
keywords:
|
||||
- grafana
|
||||
- alerting
|
||||
- images
|
||||
- notifications
|
||||
title: Images in notifications
|
||||
---
|
||||
|
||||
# Images in notifications
|
||||
|
||||
Images in notifications helps recipients of alert notifications better understand why an alert has fired or resolved by including an image of the panel for the Grafana managed alert rule.
|
||||
|
||||
> **Note**: Images in notifications are not available for Grafana Mimir and Loki managed alert rules, or when Grafana is set up to send alert notifications to an external Alertmanager.
|
||||
|
||||
If Grafana is set up to send images in notifications, it takes a screenshot of the panel for the Grafana managed alert rule when either of the following happen:
|
||||
|
||||
1. The alert rule transitions from pending to firing
|
||||
2. The alert rule transitions from firing to OK
|
||||
|
||||
Images are stored in the [data]({{< relref "../setup-grafana/configure-grafana/#paths" >}}) path and so Grafana must have write-access to this path. If Grafana cannot write to this path then taken screenshots cannot be saved to disk and an error will be logged for each failed screenshot attempt. In addition to storing images on disk, Grafana can also store the image in an external image store such as Amazon S3, Azure Blob Storage, Google Cloud Storage and even Grafana where screenshots are stored in `public/img/attachments`. Screenshots older than `temp_data_lifetime` are deleted from disk but not the external image store. If Grafana is the external image store then screenshots are deleted from `data` but not from `public/img/attachments`.
|
||||
|
||||
> **Note**: It is recommended to use an external image store is used for images in notifications as not all contact points supported uploading images from disk. It is also possible that the image on disk is deleted before an alert notification is sent if `temp_data_lifetime` is less than the `group_wait` and `group_interval` options used in Alertmanager.
|
||||
|
||||
## Requirements
|
||||
|
||||
To use images in notifications, Grafana must be set up to use [image rendering]({{< relref "../setup-grafana/image-rendering/" >}}). It is also recommended that Grafana is set up to upload images to an [external image store]({{< relref "../setup-grafana/configure-grafana/#external_image_storage" >}}) such as Amazon S3, Azure Blob Storage, Google Cloud Storage or even Grafana.
|
||||
|
||||
## Configuration
|
||||
|
||||
If Grafana has been set up to use [image rendering]({{< relref "../setup-grafana/image-rendering/" >}}) images in notifications can be turned on via the `capture` option in `[unified_alerting.screenshots]`:
|
||||
|
||||
# Enable screenshots in notifications. This option requires a remote HTTP image rendering service. Please
|
||||
# see [rendering] for further configuration options.
|
||||
capture = true
|
||||
|
||||
It is recommended that `max_concurrent_screenshots` is set to a value that is less than or equal to `concurrent_render_request_limit`. The default value for both `max_concurrent_screenshots` and `concurrent_render_request_limit` is `5`:
|
||||
|
||||
# The maximum number of screenshots that can be taken at the same time. This option is different from
|
||||
# concurrent_render_request_limit as max_concurrent_screenshots sets the number of concurrent screenshots
|
||||
# that can be taken at the same time for all firing alerts where as concurrent_render_request_limit sets
|
||||
# the total number of concurrent screenshots across all Grafana services.
|
||||
max_concurrent_screenshots = 5
|
||||
|
||||
If Grafana has been set up to use an external image store, `upload_external_image_storage` should be set to `true`:
|
||||
|
||||
# Uploads screenshots to the local Grafana server or remote storage such as Azure, S3 and GCS. Please
|
||||
# see [external_image_storage] for further configuration options. If this option is false, screenshots
|
||||
# will be persisted to disk for up to temp_data_lifetime.
|
||||
upload_external_image_storage = false
|
||||
|
||||
Restart Grafana for the changes to take affect.
|
||||
|
||||
## Supported notifiers
|
||||
|
||||
Images in notifications are supported in the following notifiers and additional support will be added in the future:
|
||||
|
||||
| Name | Upload images from disk | Include images from URL |
|
||||
| ----------------------- | ----------------------- | ----------------------- |
|
||||
| DingDing | No | No |
|
||||
| Discord | Yes | Yes |
|
||||
| Email | Yes | Yes |
|
||||
| Google Hangouts Chat | No | Yes |
|
||||
| Kafka | No | No |
|
||||
| Line | No | No |
|
||||
| Microsoft Teams | No | Yes |
|
||||
| Opsgenie | No | Yes |
|
||||
| Pagerduty | No | Yes |
|
||||
| Prometheus Alertmanager | No | No |
|
||||
| Pushover | No | No |
|
||||
| Sensu Go | No | No |
|
||||
| Slack | No | Yes |
|
||||
| Telegram | No | No |
|
||||
| Threema | No | No |
|
||||
| VictorOps | No | No |
|
||||
| Webhook | No | Yes |
|
||||
| WeCom | No | No |
|
||||
|
||||
Include images from URL refers to using the external image store.
|
||||
|
||||
## Metrics
|
||||
|
||||
Grafana provides the following metrics to observe the performance and failure rate of images in notifications.
|
||||
For example, if a screenshot could not be taken within the expected time (10 seconds) then the counter `grafana_screenshot_failures_total` is updated.
|
||||
|
||||
- `grafana_screenshot_cache_hits_total`
|
||||
- `grafana_screenshot_cache_misses_total`
|
||||
- `grafana_screenshot_duration_seconds`
|
||||
- `grafana_screenshot_failures_total`
|
||||
- `grafana_screenshot_successes_total`
|
||||
- `grafana_screenshot_upload_failures_total`
|
||||
- `grafana_screenshot_upload_successes_total`
|
||||
@@ -1,27 +1,33 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/migrating-alerts/
|
||||
description: Migrate Grafana alerts
|
||||
title: Migrate to Grafana Alerting
|
||||
weight: 113
|
||||
description: Upgrade Grafana alerts
|
||||
title: Upgrade to Grafana Alerting
|
||||
weight: 101
|
||||
---
|
||||
|
||||
# Migrate to Grafana Alerting
|
||||
# Upgrade to Grafana Alerting
|
||||
|
||||
Grafana Alerting is the default for new Cloud, Enterprise, and OSS installations. The new installations will only show the Grafana Alerting icon in the left navigation panel.
|
||||
Grafana Alerting is enabled by default for new installations or existing installations whether or not legacy alerting is configured.
|
||||
|
||||
Existing installations that upgrade to v9.0 will have Grafana Alerting enabled by default.
|
||||
> **Note**: We recommend that Grafana Enterprise customers with more than a dozen Grafana dashboard alert rules do not upgrade and remain on legacy alerting for now by [opting out]({{< relref "opt-out/" >}}). If you do want to upgrade to Grafana Alerting, contact customer support.
|
||||
|
||||
| Grafana instance upgraded to v 90 | |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Cloud | Existing Cloud installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting plugin icon and the new Grafana Alerting icon. During upgrade, existing alerts from the Cloud alerting plugin are migrated to Grafana Alerting. Once migration is complete, you can access aman manage the older alerts from the new alerting Grafana Alerting icon in the navigation panel. The (older) Cloud alerting plugin is uninstalled from your cloud instance. Contact customer support if you **do not wish** to migrate to Grafana Alerting for your Cloud stack. If you choose to use legacy alerting, use the You will see the new Grafana Alerting icon as well as the old Cloud alerting plugin in the left navigation panel. |
|
||||
| Enterprise | Existing Enterprise instances using legacy alerting will have both the old (marked as legacy) and the new alerting icons in the navigation panel. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
|
||||
| OSS | Existing OSS installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting icon (marked as legacy) and the new Grafana Alerting icon. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
|
||||
Existing installations that do not use legacy alerting will have Grafana Alerting enabled by default unless alerting is disabled in the configuration.
|
||||
|
||||
During migration from legacy alerting to unified alerting, the legacy alerts are updated to the new alerts type, as a result, the user does not lose alerts or alerting data. However, if a user rolls back to legacy alerting after having migrated to unified alerting, they will only get the legacy alerts they had right before migration.
|
||||
Likewise, existing installations that use legacy alerting will be automatically upgraded to Grafana Alerting unless you have [opted out]({{< relref "opt-out/" >}}) of Grafana Alerting before migration takes place. During the upgrade, legacy alerts are migrated to the new alerts type and no alerts or alerting data are lost.
|
||||
|
||||
## Roll back to legacy alerting
|
||||
Once the upgrade has taken place, you still have the option to [roll back]({{< relref "roll-back/" >}}) to legacy alerting. However, we do not recommend choosing this option. If you do choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place. All new alerts and changes made exclusively in Grafana Alerting will be deleted.
|
||||
|
||||
Although we encourage you to use Grafana Alerting, roll back to legacy alerting is supported in Grafana 9. Rolling back can result in data loss (you will loose all alerts that you created using Grafana Alerting). This is applicable to the fresh installation as well as upgraded setups.
|
||||
> **Note**: Cloud customers, who do not want to upgrade to Grafana Alerting, should contact customer support.
|
||||
|
||||
If you have opted out or rolled back, you can always choose to [opt in]({{< relref "opt-in/" >}}) to Grafana Alerting at a later point in time.
|
||||
|
||||
The following table provides details on the upgrade for Cloud, Enterprise, and OSS installations and the new Grafana Alerting UI.
|
||||
|
||||
| Grafana instance upgraded to 9.0 | |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Cloud | Existing Cloud installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting plugin icon and the new Grafana Alerting icon. During upgrade, existing alerts from the Cloud alerting plugin are migrated to Grafana Alerting. Once migration is complete, you can access and manage the older alerts from the new alerting Grafana Alerting icon in the navigation panel. The (older) Cloud alerting plugin is uninstalled from your cloud instance. Contact customer support if you **do not wish** to migrate to Grafana Alerting for your Cloud stack. If you choose to use legacy alerting, use the You will see the new Grafana Alerting icon as well as the old Cloud alerting plugin in the left navigation panel. |
|
||||
| Enterprise | Existing Enterprise instances using legacy alerting will have both the old (marked as legacy) and the new alerting icons in the navigation panel. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
|
||||
| OSS | Existing OSS installations with legacy dashboard alerting will have two alerting icons in the left navigation panel - the old alerting icon (marked as legacy) and the new Grafana Alerting icon. During upgrade, existing legacy alerts are migrated to Grafana Alerting. If you wish, you can [opt-out]({{< relref "opt-out/" >}}) of Grafana Alerting and roll back to legacy alerting. In that case, you can manage your legacy alerts from the alerting icon marked as legacy. |
|
||||
|
||||
> **Note:** Legacy alerting will be deprecated in a future release (v10).
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/migrating-alerts/disable-alerting/
|
||||
- /docs/grafana/latest/alerting/disable-alerting/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/disable-alerting/
|
||||
description: Disable alerting in Grafana
|
||||
title: Disable alerting in Grafana
|
||||
weight: 105
|
||||
---
|
||||
|
||||
# Disable alerting in Grafana
|
||||
|
||||
To disable alerting in Grafana entirely (including both legacy and Grafana Alerting), enter the following in your configuration:
|
||||
|
||||
```
|
||||
[alerting]
|
||||
enabled = false
|
||||
|
||||
[unified_alerting]
|
||||
enabled = false
|
||||
```
|
||||
|
||||
If at any time you want to turn alerting back on, you can opt in.
|
||||
@@ -4,13 +4,14 @@ aliases:
|
||||
- /docs/grafana/latest/alerting/migrating-legacy-alerts/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
|
||||
description: Migrate legacy dashboard alerts
|
||||
title: Migrating legacy dashboard alerts
|
||||
weight: 114
|
||||
title: Differences and limitations
|
||||
weight: 106
|
||||
---
|
||||
|
||||
# Migrating legacy dashboard alerts
|
||||
# Differences and limitations
|
||||
|
||||
When Grafana Alerting is enabled or Grafana is upgraded to the latest version, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts. This topic explains how legacy dashboard alerts are migrated and some limitations.
|
||||
When Grafana Alerting is enabled or upgraded to Grafana 9.0 or later, existing legacy dashboard alerts migrate in a format compatible with the Grafana Alerting. In the Alerting page of your Grafana instance, you can view the migrated alerts alongside any new alerts.
|
||||
This topic explains how legacy dashboard alerts are migrated and some limitations of the migration.
|
||||
|
||||
> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana Alerting for your Cloud stack.
|
||||
|
||||
@@ -24,5 +25,7 @@ Read and write access to legacy dashboard alerts and Grafana alerts are governed
|
||||
|
||||
Notification channels are migrated to an Alertmanager configuration with the appropriate routes and receivers. Default notification channels are added as contact points to the default route. Notification channels not associated with any Dashboard alert go to the `autogen-unlinked-channel-recv` route.
|
||||
|
||||
## Limitations
|
||||
|
||||
Since `Hipchat` and `Sensu` notification channels are no longer supported, legacy alerts associated with these channels are not automatically migrated to Grafana Alerting. Assign the legacy alerts to a supported notification channel so that you continue to receive notifications for those alerts.
|
||||
Silences (expiring after one year) are created for all paused dashboard alerts.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/migrating-alerts/opt-in/
|
||||
- /docs/grafana/latest/alerting/opt-in/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
|
||||
description: Opt in to Grafana Alerting
|
||||
title: Opt in to Grafana Alerting
|
||||
weight: 104
|
||||
---
|
||||
|
||||
# Opt in to Grafana Alerting
|
||||
|
||||
If you have previously disabled alerting in Grafana, or opted out of Grafana Alerting and have decided that you would now like to use Grafana Alerting, you can choose to opt in at any time.
|
||||
|
||||
If you have been using legacy alerting up until now your existing alerts will be migrated to the new alerts type and no alerts or alerting data are lost. Even if you choose to opt in to Grafana Alerting, you can roll back to legacy alerting at any time.
|
||||
|
||||
To opt in to Grafana Alerting, enter the following in your configuration:
|
||||
|
||||
```
|
||||
[alerting]
|
||||
enabled = false
|
||||
|
||||
[unified_alerting]
|
||||
enabled = true
|
||||
```
|
||||
@@ -3,41 +3,47 @@ aliases:
|
||||
- /docs/grafana/latest/alerting/migrating-alerts/opt-out/
|
||||
- /docs/grafana/latest/alerting/opt-in/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/opt-in/
|
||||
description: Disable Grafana alerts
|
||||
title: Opt-out of Grafana Alerting
|
||||
weight: 113
|
||||
description: Opt out of Grafana Alerting
|
||||
title: Opt out of Grafana Alerting
|
||||
weight: 102
|
||||
---
|
||||
|
||||
# Opt-out to Grafana Alerting in OSS
|
||||
# Opt out of Grafana Alerting
|
||||
|
||||
This topic discusses how to disable Grafana Alerting and migrate to legacy dashboard alerting. It also provides guidance on how to enable Grafana Alerting once you are ready to migrate to Grafana Alerting.
|
||||
If you have an existing installation, you can opt out of alerting in its entirety or opt out of Grafana Alerting in favor of using legacy alerting.
|
||||
|
||||
Existing installations that do not use legacy alerting will have Grafana Alerting enabled by default unless alerting is disabled in the configuration. To keep alerting disabled:
|
||||
|
||||
1. Go to your custom configuration file ($WORKING_DIR/conf/custom.ini).
|
||||
1. Enter the following in your configuration:
|
||||
|
||||
```
|
||||
[alerting]
|
||||
enabled = false
|
||||
|
||||
[unified_alerting]
|
||||
enabled = false
|
||||
```
|
||||
|
||||
3. Restart Grafana for the configuration changes to take effect.
|
||||
|
||||
If at any time you want to turn alerting back on, you can do so.
|
||||
|
||||
Existing installations that use legacy alerting will automatically be upgraded to Grafana Alerting unless you have opted-out of Grafana Alerting before migration takes place. During the upgrade, legacy alerts are migrated to the new alerts type and no alerts or alerting data are lost. To keep using legacy alerting and disable Grafana Alerting:
|
||||
|
||||
1. Go to your custom configuration file ($WORKING_DIR/conf/custom.ini).
|
||||
2. Enter the following in your configuration:
|
||||
|
||||
```
|
||||
[alerting]
|
||||
enabled = false
|
||||
|
||||
[unified_alerting]
|
||||
enabled = true
|
||||
```
|
||||
|
||||
Installations that have been migrated to Grafana Alerting can [roll back]({{< relref "roll-back/" >}}) to legacy alerting at any time.
|
||||
|
||||
> **Note:** This topic is only relevant for OSS and Enterprise customers. Contact customer support to enable or disable Grafana Alerting for your Grafana Cloud stack.
|
||||
|
||||
## Before you begin
|
||||
|
||||
We recommend that you backup Grafana's database. If you are using PostgreSQL as the backend database, then the minimum required version is 9.5.
|
||||
|
||||
## Opt-out of Grafana alerts
|
||||
|
||||
To opt-out of Grafana alerts and roll back to legacy dashboard alerting:
|
||||
|
||||
1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [Grafana Alerting]({{< relref "../../setup-grafana/configure-grafana/#unified_alerting" >}}) section.
|
||||
1. Set the `enabled` property to `false`.
|
||||
1. For [legacy dashboard alerting]({{< relref "../../setup-grafana/configure-grafana/#alerting" >}}), set the `enabled` flag to `true`.
|
||||
1. Restart Grafana for the configuration changes to take effect.
|
||||
|
||||
> **Note:** Rolling back from Grafana to legacy alerting can result in data loss. This is applicable to the fresh installation as well as upgraded setups.
|
||||
|
||||
## Opt-in to Grafana Alerting
|
||||
|
||||
When you are ready to make the switch, the following procedure will help you migrate to Grafana Alerting.
|
||||
|
||||
To opt-in to Grafana alerts:
|
||||
|
||||
1. In your custom configuration file ($WORKING_DIR/conf/custom.ini), go to the [unified alerts]({{< relref "../../setup-grafana/configure-grafana/#unified_alerting" >}}) section.
|
||||
1. Set the `enabled` property to `true`.
|
||||
1. Next, for [legacy dashboard alerting]({{< relref "../../setup-grafana/configure-grafana/#alerting" >}}), set the `enabled` flag to `false`.
|
||||
1. Restart Grafana for the configuration changes to take effect.
|
||||
|
||||
> **Note:** The `ngalert` toggle previously used to enable or disable Grafana Alerting is no longer available.
|
||||
The `ngalert` toggle previously used to enable or disable Grafana Alerting is no longer available.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/latest/alerting/migrating-alerts/roll-back/
|
||||
- /docs/grafana/latest/alerting/opt-in/
|
||||
- /docs/grafana/latest/alerting/unified-alerting/roll-back/
|
||||
description: Roll back to legacy alerting
|
||||
title: Roll back to legacy alerting
|
||||
weight: 103
|
||||
---
|
||||
|
||||
# Roll back to legacy alerting
|
||||
|
||||
Once the upgrade has taken place, you still have the option to roll back to legacy alerting. If you choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place.
|
||||
|
||||
All new alerts and changes made exclusively in Grafana Alerting will be deleted.
|
||||
|
||||
To roll back to legacy alerting, enter the following in your configuration:
|
||||
|
||||
```
|
||||
force_migration = true
|
||||
|
||||
[alerting]
|
||||
enabled = true
|
||||
|
||||
[unified_alerting]
|
||||
enabled = false
|
||||
```
|
||||
|
||||
> **Note**: We do not recommend this option. If you choose to roll back, Grafana will restore your alerts to the alerts you had at the point in time when the upgrade took place. All new alerts and changes made exclusively in Grafana Alerting will be deleted.
|
||||
@@ -2,7 +2,7 @@
|
||||
title = "Performance considerations"
|
||||
description = "Understanding alerting performance"
|
||||
keywords = ["grafana", "alerting", "performance"]
|
||||
weight = 100
|
||||
weight = 555
|
||||
+++
|
||||
|
||||
# Alerting performance considerations
|
||||
|
||||
@@ -174,10 +174,10 @@ To display the service graph:
|
||||
|
||||
- [Configure the Grafana Agent](https://grafana.com/docs/tempo/next/grafana-agent/service-graphs/#quickstart) to generate service graph data
|
||||
- Link a Prometheus datasource in the Tempo datasource settings.
|
||||
- Navigate to [Explore]({{< relref "../explore/" >}})
|
||||
- Select the Tempo datasource
|
||||
- Select the **Service Graph** query type and run the query
|
||||
- Optionally, filter by service name
|
||||
- Navigate to [Explore]({{< relref "../explore/" >}}).
|
||||
- Select the Tempo datasource.
|
||||
- Select the **Service Graph** query type and run the query.
|
||||
- (Optional): filter by service name.
|
||||
|
||||
You can pan and zoom the view with buttons or you mouse. For details about the visualization, refer to [Node graph panel](https://grafana.com/docs/grafana/latest/panels/visualizations/node-graph/).
|
||||
|
||||
@@ -192,6 +192,25 @@ The color of each circle represents the percentage of requests in each of the fo
|
||||
|
||||
Click on the service to see a context menu with additional links for quick navigation to other relevant information.
|
||||
|
||||
## APM table
|
||||
|
||||
The APM (Application Performance Management) table allows you to view several APM metrics out of the box.
|
||||
|
||||
To display the APM table:
|
||||
|
||||
1. Activate the tempoApmTable feature flag in your ini file.
|
||||
1. Link a Prometheus datasource in the Tempo datasource settings.
|
||||
1. Navigate to [Explore]({{< relref "../explore/_index.md" >}}).
|
||||
1. Select the Tempo datasource.
|
||||
1. Select the **Service Graph** query type and run the query.
|
||||
1. (Optional): filter your results.
|
||||
|
||||
Note: The metric traces_spanmetrics_calls_total is used to display the name, rate & error rate columns and traces_spanmetrics_duration_seconds_bucket is used to display the duration column (these metrics will need to exist in your Prometheus datasource).
|
||||
|
||||
Click a row in the rate, error rate, or duration columns to open a query in Prometheus with the span name of that row automatically set in the query. Click a row in the links column to open a query in Tempo with the span name of that row automatically set in the query.
|
||||
|
||||
{{< figure src="/static/img/docs/tempo/apm-table.png" class="docs-image--no-shadow" max-width="500px" caption="Screenshot of the Tempo APM table" >}}
|
||||
|
||||
## Linking Trace ID from logs
|
||||
|
||||
You can link to Tempo trace from logs in Loki or Elastic by configuring an internal link. See the [Derived fields]({{< relref "loki/#derived-fields" >}}) section in the [Loki data source]({{< relref "loki/" >}}) or [Data links]({{< relref "elasticsearch/#data-links" >}}) section in the [Elastic data source]({{< relref "elasticsearch/" >}}) for configuration instructions.
|
||||
|
||||
@@ -15,26 +15,26 @@ The following tables list permissions associated with basic and fixed roles.
|
||||
|
||||
## Basic role assignments
|
||||
|
||||
| Basic role | Associated fixed roles | Description |
|
||||
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Grafana Admin | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer` | Default [Grafana server administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions/#grafana-server-administrators" >}}) assignments. |
|
||||
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixes:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:editor`<br>`fixed:apikeys:reader`<br>`fixed:apikeys:writer`<br>`fixed:alerting:provisioning` | Default [Grafana organization administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions/#organization-users-and-permissions" >}}) assignments. |
|
||||
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled<br>`fixed:alerting:editor` | Default [Editor]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions/#organization-users-and-permissions" >}}) assignments. |
|
||||
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader` | Default [Viewer]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions/#organization-users-and-permissions" >}}) assignments. |
|
||||
| Basic role | Associated fixed roles | Description |
|
||||
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Grafana Admin | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer` | Default [Grafana server administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions/#grafana-server-administrators" >}}) assignments. |
|
||||
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixes:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:writer`<br>`fixed:apikeys:reader`<br>`fixed:apikeys:writer`<br>`fixed:alerting.provisioning:writer` | Default [Grafana organization administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions/#organization-users-and-permissions" >}}) assignments. |
|
||||
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled<br>`fixed:alerting:writer` | Default [Editor]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions/#organization-users-and-permissions" >}}) assignments. |
|
||||
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader` | Default [Viewer]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions/#organization-users-and-permissions" >}}) assignments. |
|
||||
|
||||
## Fixed role definitions
|
||||
|
||||
| Fixed role | Permissions | Description |
|
||||
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `fixed:alerting.instances:editor` | All permissions from `fixed:alerting.instances:reader` and<br> `alert.instances:create`<br>`alert.instances:write` for organization scope <br> `alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) |
|
||||
| `fixed:alerting.instances:writer` | All permissions from `fixed:alerting.instances:reader` and<br> `alert.instances:create`<br>`alert.instances:write` for organization scope <br> `alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) |
|
||||
| `fixed:alerting.instances:reader` | `alert.instances:read` for organization scope <br> `alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) |
|
||||
| `fixed:alerting.notifications:editor` | All permissions from `fixed:alerting.notifications:reader` and<br>`alert.notifications:write`for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) |
|
||||
| `fixed:alerting.notifications:writer` | All permissions from `fixed:alerting.notifications:reader` and<br>`alert.notifications:write`for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) |
|
||||
| `fixed:alerting.notifications:reader` | `alert.notifications:read` for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) |
|
||||
| `fixed:alerting.rules:editor` | All permissions from `fixed:alerting.rules:reader` and <br> `alert.rule:create` <br> `alert.rule:write` <br> `alert.rule:delete` for scope `folders:*` <br> `alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) |
|
||||
| `fixed:alerting.rules:writer` | All permissions from `fixed:alerting.rules:reader` and <br> `alert.rule:create` <br> `alert.rule:write` <br> `alert.rule:delete` for scope `folders:*` <br> `alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) |
|
||||
| `fixed:alerting.rules:reader` | `alert.rule:read` for scope `folders:*` <br> `alert.rules.external:read` for scope `datasources:*` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) |
|
||||
| `fixed:alerting:editor` | All permissions from `fixed:alerting.rules:editor` <br>`fixed:alerting.instances:editor`<br>`fixed:alerting.notifications:editor` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) |
|
||||
| `fixed:alerting:writer` | All permissions from `fixed:alerting.rules:writer` <br>`fixed:alerting.instances:writer`<br>`fixed:alerting.notifications:writer` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) |
|
||||
| `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader` <br>`fixed:alerting.instances:reader`<br>`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) |
|
||||
| `fixed:alerting:provisioning` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) |
|
||||
| `fixed:alerting.provisioning:writer` | `alert.provisioning:read` and `alert.provisioning:write` | Create, update and delete Grafana alert rules, notification policies, contact points, templates, etc via provisioning API. [\*](#alerting-roles) |
|
||||
| `fixed:annotations.dashboard:writer` | `annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. |
|
||||
| `fixed:annotations:reader` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. |
|
||||
| `fixed:annotations:writer` | All permissions from `fixed:annotations:reader` <br>`annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. |
|
||||
@@ -88,6 +88,6 @@ Access to Grafana alert rules is an intersection of many permissions:
|
||||
- Permission to read a folder. For example, the fixed role `fixed:folders:reader` includes the action `folders:read` and a folder scope `folders:id:`.
|
||||
- Permission to query **all** data sources that a given alert rule uses. If a user cannot query a given data source, they cannot see any alert rules that query that data source.
|
||||
|
||||
There is only one exclusion at this moment. Role `fixed:alerting:provisioning` does not require user to have any additional permissions and provides access to all aspects of the alerting configuration via special provisioning API.
|
||||
There is only one exclusion at this moment. Role `fixed:alerting.provisioning:writer` does not require user to have any additional permissions and provides access to all aspects of the alerting configuration via special provisioning API.
|
||||
|
||||
For more information about the permissions required to access alert rules, refer to [Create a custom role to access alerts in a folder]({{< relref "plan-rbac-rollout-strategy/#create-a-custom-role-to-access-alerts-in-a-folder" >}}).
|
||||
|
||||
@@ -81,29 +81,7 @@ module.exports.getWebpackConfig = (config, options) => ({
|
||||
});
|
||||
```
|
||||
|
||||
Please refer to the webpack build error messages or the [official migration guide](https://webpack.js.org/migrate/5/) for assistance with fallbacks.
|
||||
|
||||
**Which issue(s) this PR fixes**:
|
||||
|
||||
<!--
|
||||
|
||||
- Automatically closes linked issue when the Pull Request is merged.
|
||||
|
||||
Usage: "Fixes #<issue number>", or "Fixes (paste link of issue)"
|
||||
|
||||
-->
|
||||
|
||||
Fixes #
|
||||
|
||||
**Special notes for your reviewer**:
|
||||
|
||||
It does not bump the following dependencies to the very latest due to the latest versions being ES modules:
|
||||
|
||||
- ora
|
||||
- globby
|
||||
- execa
|
||||
- chalk
|
||||
Issue [#47826](https://github.com/grafana/grafana/issues/47826)
|
||||
Please refer to the webpack build error messages or the [official migration guide](https://webpack.js.org/migrate/5/) for assistance with fallbacks. Issue [#47826](https://github.com/grafana/grafana/issues/47826)
|
||||
|
||||
We have changed the internals of `backendSrv.fetch()` to throw an error when the response is an incorrect JSON.
|
||||
|
||||
|
||||
@@ -1189,7 +1189,7 @@ For more information about the Grafana alerts, refer to [About Grafana Alerting]
|
||||
|
||||
### enabled
|
||||
|
||||
Enable the Unified Alerting sub-system and interface. When enabled we'll migrate all of your alert rules and notification channels to the new system. New alert rules will be created and your notification channels will be converted into an Alertmanager configuration. Previous data is preserved to enable backwards compatibility but new data is removed. The default value is `false`.
|
||||
Enable or disable Grafana Alerting. If enabled, we’ll migrate all your alert rules and notification channels to the new system as alert rules and notification channels you had previously defined will be converted into an Alertmanager configuration. Legacy alerting data is preserved to enable backwards compatibility. If disabled, all your legacy alerting data will be available again, but the data you created using Grafana Alerting will be deleted. Set force_migration=true to avoid deletion of data. The default value is `true`.
|
||||
|
||||
Alerting Rules migrated from dashboards and panels will include a link back via the `annotations`.
|
||||
|
||||
@@ -1267,6 +1267,24 @@ The interval string is a possibly signed sequence of decimal numbers, followed b
|
||||
|
||||
<hr>
|
||||
|
||||
## [unified_alerting.screenshots]
|
||||
|
||||
For more information about screenshots, refer to [Images in notifications]({{< relref "../../alerting/images-in-notifications" >}}).
|
||||
|
||||
### capture
|
||||
|
||||
Enable screenshots in notifications. This option requires a remote HTTP image rendering service. Please see `[rendering]` for further configuration options.
|
||||
|
||||
### max_concurrent_screenshots
|
||||
|
||||
The maximum number of screenshots that can be taken at the same time. This option is different from `concurrent_render_request_limit` as `max_concurrent_screenshots` sets the number of concurrent screenshots that can be taken at the same time for all firing alerts where as concurrent_render_request_limit sets the total number of concurrent screenshots across all Grafana services.
|
||||
|
||||
### upload_external_image_storage
|
||||
|
||||
Uploads screenshots to the local Grafana server or remote storage such as Azure, S3 and GCS. Please see `[external_image_storage]` for further configuration options. If this option is false then screenshots will be persisted to disk for up to `temp_data_lifetime`.
|
||||
|
||||
<hr>
|
||||
|
||||
## [alerting]
|
||||
|
||||
For more information about the legacy dashboard alerting feature in Grafana, refer to [Alerts overview]({{< relref "../../alerting/" >}}).
|
||||
|
||||
@@ -181,10 +181,11 @@ This is a partial list of notable breaking changes. For the complete list, see o
|
||||
|
||||
Fine-grained access control is now called "Role-based access control". As part of the Grafana 9.0 release, the service is generally available, and there are several breaking changes:
|
||||
|
||||
Built-in roles are now called basic roles, and they now consist of permissions, not roles
|
||||
The Terraform builtin_role_assignment resource is going away.
|
||||
Grafana provisioning has a new schema, please refer to the documentation for more information.
|
||||
Basic roles no longer support permission inheritance. Previously, when permissions of a Viewer basic role were modified, it was propagated to the Editor and Admin basic roles. With the Grafana 9.0 release, this is not the case anymore.
|
||||
- Built-in roles are now called basic roles. They now consist of permissions, not roles.
|
||||
- The Terraform builtin_role_assignment resource is going away.
|
||||
- Grafana provisioning has a new schema. Please refer to the documentation for more information.
|
||||
- Basic roles no longer support permission inheritance. Previously, when permissions of a Viewer basic role were modified, it was propagated to the Editor and Admin basic roles. With the Grafana 9.0 release, this is not the case anymore.
|
||||
- Several role-based access control actions have been renamed. All the database entries that use legacy action names will be migrated to use the new names, but provisioning files and scripts will have to be updated by the user. This change also means that if Grafana is downgraded from 9.0 to a lower version, some role-based access control permissions will not be resolved correctly.
|
||||
|
||||
### Loki: logs data format changed
|
||||
|
||||
@@ -215,6 +216,25 @@ In the InfluxDB data source, browser access mode was deprecated in grafana 8.0.0
|
||||
|
||||
The rename by regex transformation has been improved to allow global patterns of the form `/<stringToReplace>/g`. Depending on the regex match used, this may cause some transformations to behave slightly differently. You can guarantee the same behavior as before by wrapping the match string in forward slashes (/), for example, (._) would become /(._)/. ([Github Issue #48179](https://github.com/grafana/grafana/pull/48179))
|
||||
|
||||
### Clock Panel
|
||||
|
||||
We have updated [clock panel](https://grafana.com/grafana/plugins/grafana-clock-panel/) to version `2.0.0` to make it Compatible with Grafana 9. The previous version `1.3.1` will cause the Grafana 9 to [crash](https://github.com/grafana/clock-panel/issues/106) when being used in a dashboard, we encourage you to update the panel before migrating to Grafana 9.
|
||||
|
||||
### Plugins: Most relevant breaking changes
|
||||
|
||||
- `getColorForTheme` was removed, use `theme.visualization.getColorByName` instead [PR #49519](https://github.com/grafana/grafana/pull/49519)
|
||||
- `TextDisplayOptions` was removed, use `VizTextDisplayOptions` instead [PR #49705](https://github.com/grafana/grafana/pull/49705)
|
||||
- We have changed the internals of `backendSrv.fetch()` to throw an error when the response is an incorrect JSON. Make sure to handle possible errors on the callsite where using `backendSrv.fetch()` (or any other `backendSrv` methods) [PR #47493](https://github.com/grafana/grafana/pull/47493)
|
||||
- We have removed the deprecated `getFormStyles` function from [grafana-ui](https://www.npmjs.com/package/@grafana/ui). Use `GrafanaTheme2` and the `useStyles2` hook instead [PR #49945](https://github.com/grafana/grafana/pull/49945)
|
||||
- We have removed the deprecated `/api/tsdb/query` metrics endpoint. Use `/api/ds/query` instead [PR #49916](https://github.com/grafana/grafana/pull/49916)
|
||||
|
||||
You can find the complete list of breaking changes in the links below. Please check them out for more details and see if there is anything affecting your plugin
|
||||
|
||||
- https://grafana.com/docs/grafana/next/release-notes/release-notes-9-0-0-beta1/
|
||||
- https://grafana.com/docs/grafana/next/release-notes/release-notes-9-0-0-beta2/
|
||||
- https://grafana.com/docs/grafana/next/release-notes/release-notes-9-0-0-beta3/
|
||||
- https://grafana.com/docs/grafana/next/release-notes/release-notes-9-0-0
|
||||
|
||||
## A note on Grafana Enterprise licensing
|
||||
|
||||
When we release Grafana 9.0 on June 14th, Grafana will no longer enforce viewers and editor-admins differently. That means that regardless of whether your Grafana Enterprise license is tiered or combined, instead of seeing this on the Stats & Licensing page:
|
||||
|
||||
+6
-6
@@ -267,12 +267,12 @@
|
||||
"@lingui/react": "3.13.3",
|
||||
"@opentelemetry/api": "1.1.0",
|
||||
"@opentelemetry/exporter-collector": "0.25.0",
|
||||
"@opentelemetry/semantic-conventions": "1.2.0",
|
||||
"@opentelemetry/semantic-conventions": "1.3.1",
|
||||
"@popperjs/core": "2.11.5",
|
||||
"@react-aria/button": "3.4.4",
|
||||
"@react-aria/dialog": "3.1.9",
|
||||
"@react-aria/focus": "3.5.5",
|
||||
"@react-aria/interactions": "3.8.4",
|
||||
"@react-aria/button": "3.5.0",
|
||||
"@react-aria/dialog": "3.2.0",
|
||||
"@react-aria/focus": "3.6.0",
|
||||
"@react-aria/interactions": "3.9.0",
|
||||
"@react-aria/menu": "3.4.4",
|
||||
"@react-aria/overlays": "3.8.2",
|
||||
"@react-aria/utils": "3.12.0",
|
||||
@@ -397,7 +397,7 @@
|
||||
"resolutions": {
|
||||
"underscore": "1.13.3",
|
||||
"@types/slate": "0.47.2",
|
||||
"@microsoft/api-extractor-model": "7.17.3",
|
||||
"@microsoft/api-extractor-model": "7.18.0",
|
||||
"@rushstack/node-core-library": "3.45.5",
|
||||
"@rushstack/rig-package": "0.3.11",
|
||||
"@rushstack/ts-command-line": "4.11.0",
|
||||
|
||||
@@ -3,7 +3,7 @@ import BaseLayer from 'ol/layer/Base';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '../themes';
|
||||
import { PanelData } from '../types';
|
||||
import { MatcherConfig, PanelData } from '../types';
|
||||
import { PanelOptionsEditorBuilder } from '../utils';
|
||||
import { RegistryItemWithOptions } from '../utils/Registry';
|
||||
|
||||
@@ -58,6 +58,9 @@ export interface MapLayerOptions<TConfig = any> {
|
||||
// Common method to define geometry fields
|
||||
location?: FrameGeometrySource;
|
||||
|
||||
// Defines which data query refId is associated with the layer
|
||||
filterData?: MatcherConfig;
|
||||
|
||||
// Common properties:
|
||||
// https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html
|
||||
// Layer opacity (0-1)
|
||||
@@ -72,6 +75,9 @@ export interface MapLayerOptions<TConfig = any> {
|
||||
*/
|
||||
export interface MapLayerHandler<TConfig = any> {
|
||||
init: () => BaseLayer;
|
||||
/**
|
||||
* The update function should only be implemented if the layer type makes use of query data
|
||||
*/
|
||||
update?: (data: PanelData) => void;
|
||||
legend?: ReactNode;
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export function createComponents(colors: ThemeColors, shadows: ThemeShadows): Th
|
||||
background: colors.mode === 'dark' ? 'rgba(63, 62, 62, 0.45)' : 'rgba(208, 209, 211, 0.24)',
|
||||
},
|
||||
sidemenu: {
|
||||
width: 48,
|
||||
width: 57,
|
||||
},
|
||||
menuTabs: {
|
||||
height: 41,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface FeatureToggles {
|
||||
queryOverLive?: boolean;
|
||||
panelTitleSearch?: boolean;
|
||||
tempoServiceGraph?: boolean;
|
||||
tempoApmTable?: boolean;
|
||||
prometheus_azure_auth?: boolean;
|
||||
prometheusAzureOverrideAudience?: boolean;
|
||||
influxdbBackendMigration?: boolean;
|
||||
@@ -56,4 +57,8 @@ export interface FeatureToggles {
|
||||
validateDashboardsOnSave?: boolean;
|
||||
prometheusWideSeries?: boolean;
|
||||
canvasPanelNesting?: boolean;
|
||||
cloudMonitoringExperimentalUI?: boolean;
|
||||
logRequestsInstrumentedAsUnknown?: boolean;
|
||||
dataConnectionsConsole?: boolean;
|
||||
internationalization?: boolean;
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
"@grafana/slate-react": "0.22.10-grafana",
|
||||
"@monaco-editor/react": "4.3.1",
|
||||
"@popperjs/core": "2.11.5",
|
||||
"@react-aria/button": "3.4.4",
|
||||
"@react-aria/dialog": "3.1.9",
|
||||
"@react-aria/focus": "3.5.5",
|
||||
"@react-aria/button": "3.5.0",
|
||||
"@react-aria/dialog": "3.2.0",
|
||||
"@react-aria/focus": "3.6.0",
|
||||
"@react-aria/menu": "3.4.4",
|
||||
"@react-aria/overlays": "3.8.2",
|
||||
"@react-stately/menu": "3.2.7",
|
||||
@@ -97,6 +97,7 @@
|
||||
"@babel/core": "7.18.2",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@mdx-js/react": "1.6.22",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rollup/plugin-commonjs": "22.0.0",
|
||||
"@rollup/plugin-node-resolve": "13.3.0",
|
||||
"@storybook/addon-a11y": "6.4.21",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import alias from '@rollup/plugin-alias';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import svg from 'rollup-plugin-svg-import';
|
||||
@@ -39,6 +40,9 @@ const buildCjsPackage = ({ env }) => {
|
||||
'@emotion/css',
|
||||
],
|
||||
plugins: [
|
||||
// rc-time-picker has a transitive dependency on component-indexof which
|
||||
// when bundled via `component-classes` imports a nonexistent `indexof` module.
|
||||
alias({ entries: [{ find: 'indexof', replacement: 'component-indexof' }] }),
|
||||
commonjs({
|
||||
include: /node_modules/,
|
||||
ignoreTryCatch: false,
|
||||
|
||||
@@ -296,7 +296,7 @@ export const getStandardOptionEditors = () => {
|
||||
const fieldName: StandardEditorsRegistryItem<string, FieldNamePickerConfigSettings> = {
|
||||
id: 'field-name',
|
||||
name: 'Field name',
|
||||
description: 'Time zone selection',
|
||||
description: 'Allows selecting a field name from a data frame',
|
||||
editor: FieldNamePicker as any,
|
||||
};
|
||||
|
||||
|
||||
+12
-10
@@ -308,20 +308,22 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
// Data sources
|
||||
apiRoute.Group("/datasources", func(datasourceRoute routing.RouteRegister) {
|
||||
idScope := datasources.ScopeProvider.GetResourceScope(ac.Parameter(":id"))
|
||||
uidScope := datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":uid"))
|
||||
nameScope := datasources.ScopeProvider.GetResourceScopeName(ac.Parameter(":name"))
|
||||
datasourceRoute.Get("/", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionRead)), routing.Wrap(hs.GetDataSources))
|
||||
datasourceRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionCreate)), quota("data_source"), routing.Wrap(hs.AddDataSource))
|
||||
datasourceRoute.Put("/:id", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":id")))), routing.Wrap(hs.UpdateDataSourceByID))
|
||||
datasourceRoute.Put("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":uid")))), routing.Wrap(hs.UpdateDataSourceByUID))
|
||||
datasourceRoute.Delete("/:id", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionDelete, datasources.ScopeProvider.GetResourceScope(ac.Parameter(":id")))), routing.Wrap(hs.DeleteDataSourceById))
|
||||
datasourceRoute.Delete("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionDelete, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":uid")))), routing.Wrap(hs.DeleteDataSourceByUID))
|
||||
datasourceRoute.Delete("/name/:name", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionDelete, datasources.ScopeProvider.GetResourceScopeName(ac.Parameter(":name")))), routing.Wrap(hs.DeleteDataSourceByName))
|
||||
datasourceRoute.Get("/:id", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionRead)), routing.Wrap(hs.GetDataSourceById))
|
||||
datasourceRoute.Get("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionRead)), routing.Wrap(hs.GetDataSourceByUID))
|
||||
datasourceRoute.Get("/name/:name", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionRead)), routing.Wrap(hs.GetDataSourceByName))
|
||||
datasourceRoute.Put("/:id", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, idScope)), routing.Wrap(hs.UpdateDataSourceByID))
|
||||
datasourceRoute.Put("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, uidScope)), routing.Wrap(hs.UpdateDataSourceByUID))
|
||||
datasourceRoute.Delete("/:id", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionDelete, idScope)), routing.Wrap(hs.DeleteDataSourceById))
|
||||
datasourceRoute.Delete("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionDelete, uidScope)), routing.Wrap(hs.DeleteDataSourceByUID))
|
||||
datasourceRoute.Delete("/name/:name", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionDelete, nameScope)), routing.Wrap(hs.DeleteDataSourceByName))
|
||||
datasourceRoute.Get("/:id", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionRead, idScope)), routing.Wrap(hs.GetDataSourceById))
|
||||
datasourceRoute.Get("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionRead, uidScope)), routing.Wrap(hs.GetDataSourceByUID))
|
||||
datasourceRoute.Get("/name/:name", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionRead, nameScope)), routing.Wrap(hs.GetDataSourceByName))
|
||||
datasourceRoute.Get("/id/:name", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionIDRead, nameScope)), routing.Wrap(hs.GetDataSourceIdByName))
|
||||
})
|
||||
|
||||
apiRoute.Get("/datasources/id/:name", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionIDRead, datasources.ScopeProvider.GetResourceScopeName(ac.Parameter(":name")))), routing.Wrap(hs.GetDataSourceIdByName))
|
||||
|
||||
apiRoute.Get("/plugins", routing.Wrap(hs.GetPluginList))
|
||||
apiRoute.Get("/plugins/:pluginId/settings", routing.Wrap(hs.GetPluginSettingByID))
|
||||
apiRoute.Get("/plugins/:pluginId/markdown/:name", routing.Wrap(hs.GetPluginMarkdown))
|
||||
|
||||
+3
-18
@@ -92,12 +92,7 @@ func (hs *HTTPServer) GetDataSourceById(c *models.ReqContext) response.Response
|
||||
return response.Error(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
filtered, err := hs.filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{query.Result})
|
||||
if err != nil || len(filtered) != 1 {
|
||||
return response.Error(404, "Data source not found", err)
|
||||
}
|
||||
|
||||
dto := hs.convertModelToDtos(c.Req.Context(), filtered[0])
|
||||
dto := hs.convertModelToDtos(c.Req.Context(), query.Result)
|
||||
|
||||
// Add accesscontrol metadata
|
||||
dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, datasources.ScopePrefix, dto.UID)
|
||||
@@ -151,12 +146,7 @@ func (hs *HTTPServer) GetDataSourceByUID(c *models.ReqContext) response.Response
|
||||
return response.Error(http.StatusInternalServerError, "Failed to query datasource", err)
|
||||
}
|
||||
|
||||
filtered, err := hs.filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{ds})
|
||||
if err != nil || len(filtered) != 1 {
|
||||
return response.Error(404, "Data source not found", err)
|
||||
}
|
||||
|
||||
dto := hs.convertModelToDtos(c.Req.Context(), filtered[0])
|
||||
dto := hs.convertModelToDtos(c.Req.Context(), ds)
|
||||
|
||||
// Add accesscontrol metadata
|
||||
dto.AccessControl = hs.getAccessControlMetadata(c, c.OrgId, datasources.ScopePrefix, dto.UID)
|
||||
@@ -397,12 +387,7 @@ func (hs *HTTPServer) GetDataSourceByName(c *models.ReqContext) response.Respons
|
||||
return response.Error(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
filtered, err := hs.filterDatasourcesByQueryPermission(c.Req.Context(), c.SignedInUser, []*models.DataSource{query.Result})
|
||||
if err != nil || len(filtered) != 1 {
|
||||
return response.Error(404, "Data source not found", err)
|
||||
}
|
||||
|
||||
dto := hs.convertModelToDtos(c.Req.Context(), filtered[0])
|
||||
dto := hs.convertModelToDtos(c.Req.Context(), query.Result)
|
||||
return response.JSON(http.StatusOK, &dto)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ const (
|
||||
WeightDashboard
|
||||
WeightExplore
|
||||
WeightAlerting
|
||||
WeightDataConnections
|
||||
WeightPlugin
|
||||
WeightConfig
|
||||
WeightAdmin
|
||||
@@ -61,7 +62,7 @@ type NavLink struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Section string `json:"section,omitempty"`
|
||||
SubTitle string `json:"subTitle,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Icon string `json:"icon,omitempty"` // Available icons can be browsed in Storybook: https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview
|
||||
Img string `json:"img,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
|
||||
@@ -245,6 +245,10 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
navTree = append(navTree, hs.buildAlertNavLinks(c)...)
|
||||
}
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagDataConnectionsConsole) {
|
||||
navTree = append(navTree, hs.buildDataConnectionsNavLink(c))
|
||||
}
|
||||
|
||||
appLinks, err := hs.getAppLinks(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -630,6 +634,58 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink
|
||||
return children
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) buildDataConnectionsNavLink(c *models.ReqContext) *dtos.NavLink {
|
||||
var children []*dtos.NavLink
|
||||
var navLink *dtos.NavLink
|
||||
|
||||
baseId := "data-connections"
|
||||
baseUrl := hs.Cfg.AppSubURL + "/" + baseId
|
||||
|
||||
children = append(children, &dtos.NavLink{
|
||||
Id: baseId + "-datasources",
|
||||
Text: "Data sources",
|
||||
Icon: "database",
|
||||
Description: "Add and configure data sources",
|
||||
Url: baseUrl + "/datasources",
|
||||
})
|
||||
|
||||
children = append(children, &dtos.NavLink{
|
||||
Id: baseId + "-plugins",
|
||||
Text: "Plugins",
|
||||
Icon: "plug",
|
||||
Description: "Manage plugins",
|
||||
Url: baseUrl + "/plugins",
|
||||
})
|
||||
|
||||
children = append(children, &dtos.NavLink{
|
||||
Id: baseId + "-cloud-integrations",
|
||||
Text: "Cloud integrations",
|
||||
Icon: "bolt",
|
||||
Description: "Manage your cloud integrations",
|
||||
Url: baseUrl + "/cloud-integrations",
|
||||
})
|
||||
|
||||
children = append(children, &dtos.NavLink{
|
||||
Id: baseId + "-recorded-queries",
|
||||
Text: "Recorded queries",
|
||||
Icon: "record-audio",
|
||||
Description: "Manage your recorded queries",
|
||||
Url: baseUrl + "/recorded-queries",
|
||||
})
|
||||
|
||||
navLink = &dtos.NavLink{
|
||||
Text: "Data Connections",
|
||||
Icon: "link",
|
||||
Id: baseId,
|
||||
Url: baseUrl,
|
||||
Children: children,
|
||||
Section: dtos.NavSectionCore,
|
||||
SortWeight: dtos.WeightDataConnections,
|
||||
}
|
||||
|
||||
return navLink
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink {
|
||||
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||
hasGlobalAccess := ac.HasGlobalAccess(hs.AccessControl, c)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -13,13 +14,57 @@ var (
|
||||
ErrRelativePath = errors.New("path cant be relative")
|
||||
ErrNonCanonicalPath = errors.New("path must be canonical")
|
||||
ErrPathTooLong = errors.New("path is too long")
|
||||
ErrPathInvalid = errors.New("path is invalid")
|
||||
ErrInvalidCharacters = errors.New("path contains unsupported characters")
|
||||
ErrPathEndsWithDelimiter = errors.New("path can not end with delimiter")
|
||||
ErrPathPartTooLong = errors.New("path part is too long")
|
||||
ErrEmptyPathPart = errors.New("path can not have empty parts")
|
||||
Delimiter = "/"
|
||||
DirectoryMimeType = "directory"
|
||||
multipleDelimiters = regexp.MustCompile(`/+`)
|
||||
pathRegex = regexp.MustCompile(`(^/$)|(^(/[A-Za-z\d!\-_.*'() ]+)+$)`)
|
||||
maxPathLength = 1024
|
||||
maxPathPartLength = 256
|
||||
)
|
||||
|
||||
func ValidatePath(path string) error {
|
||||
if !filepath.IsAbs(path) {
|
||||
return ErrRelativePath
|
||||
}
|
||||
|
||||
if path == Delimiter {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, Delimiter) {
|
||||
return ErrPathEndsWithDelimiter
|
||||
}
|
||||
|
||||
if filepath.Clean(path) != path {
|
||||
return ErrNonCanonicalPath
|
||||
}
|
||||
|
||||
if len(path) > maxPathLength {
|
||||
return ErrPathTooLong
|
||||
}
|
||||
|
||||
for _, part := range strings.Split(strings.TrimPrefix(path, Delimiter), Delimiter) {
|
||||
if strings.TrimSpace(part) == "" {
|
||||
return ErrEmptyPathPart
|
||||
}
|
||||
|
||||
if len(part) > maxPathPartLength {
|
||||
return ErrPathPartTooLong
|
||||
}
|
||||
}
|
||||
|
||||
matches := pathRegex.MatchString(path)
|
||||
if !matches {
|
||||
return ErrInvalidCharacters
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Join(parts ...string) string {
|
||||
joinedPath := Delimiter + strings.Join(parts, Delimiter)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package filestorage
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -34,3 +35,81 @@ func TestFilestorageApi_Join(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func pathPart(length int) string {
|
||||
sb := strings.Builder{}
|
||||
for i := 0; i < length; i++ {
|
||||
sb.WriteString("a")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestFilestorageApi_ValidatePath(t *testing.T) {
|
||||
var tests = []struct {
|
||||
path string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
path: "abc/file.jpg",
|
||||
expectedError: ErrRelativePath,
|
||||
},
|
||||
{
|
||||
path: "../abc/file.jpg",
|
||||
expectedError: ErrRelativePath,
|
||||
},
|
||||
{
|
||||
path: "/abc/./file.jpg",
|
||||
expectedError: ErrNonCanonicalPath,
|
||||
},
|
||||
{
|
||||
path: "/abc/../abc/file.jpg",
|
||||
expectedError: ErrNonCanonicalPath,
|
||||
},
|
||||
{
|
||||
path: "/abc//folder/file.jpg",
|
||||
expectedError: ErrNonCanonicalPath,
|
||||
},
|
||||
{
|
||||
path: "/abc/file.jpg/",
|
||||
expectedError: ErrPathEndsWithDelimiter,
|
||||
},
|
||||
{
|
||||
path: "/abc/folder/ ",
|
||||
expectedError: ErrEmptyPathPart,
|
||||
},
|
||||
{
|
||||
path: "/abc/ /file.jpg",
|
||||
expectedError: ErrEmptyPathPart,
|
||||
},
|
||||
{
|
||||
path: "/abc/" + pathPart(260) + "/file.jpg",
|
||||
expectedError: ErrPathPartTooLong,
|
||||
},
|
||||
{
|
||||
path: "/abc/" + pathPart(1050) + "/file.jpg",
|
||||
expectedError: ErrPathTooLong,
|
||||
},
|
||||
{
|
||||
path: "/abc/folder🚀/file.jpg",
|
||||
expectedError: ErrInvalidCharacters,
|
||||
},
|
||||
{
|
||||
path: "/path/with/utf/char/at/the/end.jpg�",
|
||||
expectedError: ErrInvalidCharacters,
|
||||
},
|
||||
{
|
||||
path: "/myFile/file.jpg",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if tt.expectedError == nil {
|
||||
t.Run("path "+tt.path+" should be valid", func(t *testing.T) {
|
||||
require.Nil(t, ValidatePath(tt.path))
|
||||
})
|
||||
} else {
|
||||
t.Run("path "+tt.path+" should be invalid: "+tt.expectedError.Error(), func(t *testing.T) {
|
||||
require.Equal(t, tt.expectedError, ValidatePath(tt.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
|
||||
var (
|
||||
directoryMarker = ".___gf_dir_marker___"
|
||||
pathRegex = regexp.MustCompile(`(^/$)|(^(/[A-Za-z0-9!\-_.*'() ]+)+$)`)
|
||||
)
|
||||
|
||||
type wrapper struct {
|
||||
@@ -100,37 +98,8 @@ func getName(path string) string {
|
||||
return split[len(split)-1]
|
||||
}
|
||||
|
||||
func validatePath(path string) error {
|
||||
if !filepath.IsAbs(path) {
|
||||
return ErrRelativePath
|
||||
}
|
||||
|
||||
if path == Delimiter {
|
||||
return nil
|
||||
}
|
||||
|
||||
if filepath.Clean(path) != path {
|
||||
return ErrNonCanonicalPath
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, Delimiter) {
|
||||
return ErrPathEndsWithDelimiter
|
||||
}
|
||||
|
||||
if len(path) > 1000 {
|
||||
return ErrPathTooLong
|
||||
}
|
||||
|
||||
matches := pathRegex.MatchString(path)
|
||||
if !matches {
|
||||
return ErrPathInvalid
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b wrapper) validatePath(path string) error {
|
||||
if err := validatePath(path); err != nil {
|
||||
if err := ValidatePath(path); err != nil {
|
||||
b.log.Error("Path failed validation", "path", path, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package httpclientprovider_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestForwardedCookiesMiddleware(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
allowedCookies []string
|
||||
expectedCookieHeader string
|
||||
}{
|
||||
{
|
||||
desc: "With nil allowedCookies should not populate Cookie header",
|
||||
allowedCookies: nil,
|
||||
expectedCookieHeader: "",
|
||||
},
|
||||
{
|
||||
desc: "With empty allowed cookies should not populate Cookie header",
|
||||
allowedCookies: []string{},
|
||||
expectedCookieHeader: "",
|
||||
},
|
||||
{
|
||||
desc: "When provided with allowed cookies should populate Cookie header",
|
||||
allowedCookies: []string{"c1", "c3"},
|
||||
expectedCookieHeader: "c1=1; c3=3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ctx := &testContext{}
|
||||
finalRoundTripper := ctx.createRoundTripper()
|
||||
forwarded := []*http.Cookie{
|
||||
{Name: "c1", Value: "1"},
|
||||
{Name: "c2", Value: "2"},
|
||||
{Name: "c3", Value: "3"},
|
||||
}
|
||||
mw := httpclientprovider.ForwardedCookiesMiddleware(forwarded, tc.allowedCookies)
|
||||
opts := httpclient.Options{}
|
||||
rt := mw.CreateMiddleware(opts, finalRoundTripper)
|
||||
require.NotNil(t, rt)
|
||||
middlewareName, ok := mw.(httpclient.MiddlewareName)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "forwarded-cookies", middlewareName.MiddlewareName())
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://", nil)
|
||||
require.NoError(t, err)
|
||||
res, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
if res.Body != nil {
|
||||
require.NoError(t, res.Body.Close())
|
||||
}
|
||||
require.Len(t, ctx.callChain, 1)
|
||||
require.ElementsMatch(t, []string{"final"}, ctx.callChain)
|
||||
require.Equal(t, tc.expectedCookieHeader, ctx.req.Header.Get("Cookie"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package httpclientprovider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||
)
|
||||
|
||||
const ForwardedCookiesMiddlewareName = "forwarded-cookies"
|
||||
|
||||
// ForwardedCookiesMiddleware middleware that sets Cookie header on the
|
||||
// outgoing request, if forwarded cookies configured/provided.
|
||||
func ForwardedCookiesMiddleware(forwardedCookies []*http.Cookie, allowedCookies []string) httpclient.Middleware {
|
||||
return httpclient.NamedMiddlewareFunc(ForwardedCookiesMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
|
||||
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
for _, cookie := range forwardedCookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
proxyutil.ClearCookieHeader(req, allowedCookies)
|
||||
return next.RoundTrip(req)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package httpclientprovider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const ForwardedOAuthIdentityMiddlewareName = "forwarded-oauth-identity"
|
||||
|
||||
// ForwardedOAuthIdentityMiddleware middleware that sets Authorization/X-ID-Token
|
||||
// headers on the outgoing request if an OAuth Token is provided
|
||||
func ForwardedOAuthIdentityMiddleware(token *oauth2.Token) httpclient.Middleware {
|
||||
return httpclient.NamedMiddlewareFunc(ForwardedOAuthIdentityMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
|
||||
if token == nil {
|
||||
return next
|
||||
}
|
||||
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
|
||||
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
if ok && idToken != "" {
|
||||
req.Header.Set("X-ID-Token", idToken)
|
||||
}
|
||||
|
||||
return next.RoundTrip(req)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package httpclientprovider_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func TestForwardedOAuthIdentityMiddleware(t *testing.T) {
|
||||
at := &oauth2.Token{
|
||||
AccessToken: "access-token",
|
||||
}
|
||||
tcs := []struct {
|
||||
desc string
|
||||
token *oauth2.Token
|
||||
expectedAuthorizationHeader string
|
||||
expectedIDTokenHeader string
|
||||
}{
|
||||
{
|
||||
desc: "With nil token should not populate Cookie headers",
|
||||
token: nil,
|
||||
expectedAuthorizationHeader: "",
|
||||
expectedIDTokenHeader: "",
|
||||
},
|
||||
{
|
||||
desc: "With access token set should populate Authorization header",
|
||||
token: at,
|
||||
expectedAuthorizationHeader: "Bearer access-token",
|
||||
expectedIDTokenHeader: "",
|
||||
},
|
||||
{
|
||||
desc: "With Authorization and X-ID-Token header set should populate Authorization and X-Id-Token header",
|
||||
token: at.WithExtra(map[string]interface{}{"id_token": "id-token"}),
|
||||
expectedAuthorizationHeader: "Bearer access-token",
|
||||
expectedIDTokenHeader: "id-token",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
ctx := &testContext{}
|
||||
finalRoundTripper := ctx.createRoundTripper()
|
||||
mw := httpclientprovider.ForwardedOAuthIdentityMiddleware(tc.token)
|
||||
opts := httpclient.Options{}
|
||||
rt := mw.CreateMiddleware(opts, finalRoundTripper)
|
||||
require.NotNil(t, rt)
|
||||
middlewareName, ok := mw.(httpclient.MiddlewareName)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "forwarded-oauth-identity", middlewareName.MiddlewareName())
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "http://", nil)
|
||||
require.NoError(t, err)
|
||||
res, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res)
|
||||
if res.Body != nil {
|
||||
require.NoError(t, res.Body.Close())
|
||||
}
|
||||
require.Len(t, ctx.callChain, 1)
|
||||
require.ElementsMatch(t, []string{"final"}, ctx.callChain)
|
||||
require.Equal(t, tc.expectedAuthorizationHeader, ctx.req.Header.Get("Authorization"))
|
||||
require.Equal(t, tc.expectedIDTokenHeader, ctx.req.Header.Get("X-ID-Token"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testContext struct {
|
||||
callChain []string
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
func (c *testContext) createRoundTripper() http.RoundTripper {
|
||||
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
c.callChain = append(c.callChain, "final")
|
||||
c.req = req
|
||||
return &http.Response{StatusCode: http.StatusOK}, nil
|
||||
})
|
||||
}
|
||||
@@ -5,12 +5,11 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics/metricutil"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/mwitkow/go-conntrack"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -46,6 +47,8 @@ func init() {
|
||||
|
||||
// RequestMetrics is a middleware handler that instruments the request.
|
||||
func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
|
||||
log := log.New("middleware.request-metrics")
|
||||
|
||||
return func(res http.ResponseWriter, req *http.Request, c *web.Context) {
|
||||
if strings.HasPrefix(c.Req.URL.Path, "/public/") || c.Req.URL.Path == "robots.txt" || c.Req.URL.Path == "/metrics" {
|
||||
c.Next()
|
||||
@@ -58,15 +61,18 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
|
||||
defer httpRequestsInFlight.Dec()
|
||||
c.Next()
|
||||
|
||||
handler := "unknown"
|
||||
|
||||
if routeOperation, exists := RouteOperationNameFromContext(c.Req.Context()); exists {
|
||||
handler = routeOperation
|
||||
}
|
||||
|
||||
status := rw.Status()
|
||||
code := sanitizeCode(status)
|
||||
|
||||
handler := "unknown"
|
||||
if routeOperation, exists := RouteOperationNameFromContext(c.Req.Context()); exists {
|
||||
handler = routeOperation
|
||||
} else {
|
||||
if features.IsEnabled(featuremgmt.FlagLogRequestsInstrumentedAsUnknown) {
|
||||
log.Warn("request instrumented as unknown", "path", c.Req.URL.Path, "status_code", status)
|
||||
}
|
||||
}
|
||||
|
||||
// avoiding the sanitize functions for in the new instrumentation
|
||||
// since they dont make much sense. We should remove them later.
|
||||
histogram := httpRequestDurationHistogram.
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||
goplugin "github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
@@ -41,23 +42,28 @@ func newClientConfig(executablePath string, env []string, logger log.Logger,
|
||||
// StartRendererFunc callback function called when a renderer plugin is started.
|
||||
type StartRendererFunc func(pluginID string, renderer pluginextensionv2.RendererPlugin, logger log.Logger) error
|
||||
|
||||
// StartSecretsManagerFunc callback function called when a secrets manager plugin is started.
|
||||
type StartSecretsManagerFunc func(pluginID string, secretsmanager secretsmanagerplugin.SecretsManagerPlugin, logger log.Logger) error
|
||||
|
||||
// PluginDescriptor is a descriptor used for registering backend plugins.
|
||||
type PluginDescriptor struct {
|
||||
pluginID string
|
||||
executablePath string
|
||||
managed bool
|
||||
versionedPlugins map[int]goplugin.PluginSet
|
||||
startRendererFn StartRendererFunc
|
||||
pluginID string
|
||||
executablePath string
|
||||
managed bool
|
||||
versionedPlugins map[int]goplugin.PluginSet
|
||||
startRendererFn StartRendererFunc
|
||||
startSecretsManagerFn StartSecretsManagerFunc
|
||||
}
|
||||
|
||||
// getV2PluginSet returns list of plugins supported on v2.
|
||||
func getV2PluginSet() goplugin.PluginSet {
|
||||
return goplugin.PluginSet{
|
||||
"diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{},
|
||||
"resource": &grpcplugin.ResourceGRPCPlugin{},
|
||||
"data": &grpcplugin.DataGRPCPlugin{},
|
||||
"stream": &grpcplugin.StreamGRPCPlugin{},
|
||||
"renderer": &pluginextensionv2.RendererGRPCPlugin{},
|
||||
"diagnostics": &grpcplugin.DiagnosticsGRPCPlugin{},
|
||||
"resource": &grpcplugin.ResourceGRPCPlugin{},
|
||||
"data": &grpcplugin.DataGRPCPlugin{},
|
||||
"stream": &grpcplugin.StreamGRPCPlugin{},
|
||||
"renderer": &pluginextensionv2.RendererGRPCPlugin{},
|
||||
"secretsmanager": &secretsmanagerplugin.SecretsManagerGRPCPlugin{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,3 +91,16 @@ func NewRendererPlugin(pluginID, executablePath string, startFn StartRendererFun
|
||||
startRendererFn: startFn,
|
||||
})
|
||||
}
|
||||
|
||||
// NewSecetsManagerPlugin creates a new secrets manager plugin factory used for registering a backend secrets manager plugin.
|
||||
func NewSecretsManagerPlugin(pluginID, executablePath string, startFn StartSecretsManagerFunc) backendplugin.PluginFactoryFunc {
|
||||
return newPlugin(PluginDescriptor{
|
||||
pluginID: pluginID,
|
||||
executablePath: executablePath,
|
||||
managed: false,
|
||||
versionedPlugins: map[int]goplugin.PluginSet{
|
||||
grpcplugin.ProtocolVersion: getV2PluginSet(),
|
||||
},
|
||||
startSecretsManagerFn: startFn,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -23,6 +24,7 @@ type ClientV2 struct {
|
||||
grpcplugin.DataClient
|
||||
grpcplugin.StreamClient
|
||||
pluginextensionv2.RendererPlugin
|
||||
secretsmanagerplugin.SecretsManagerPlugin
|
||||
}
|
||||
|
||||
func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugin.ClientProtocol) (pluginClient, error) {
|
||||
@@ -51,6 +53,11 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawSecretsManager, err := rpcClient.Dispense("secretsmanager")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := ClientV2{}
|
||||
if rawDiagnostics != nil {
|
||||
if diagnosticsClient, ok := rawDiagnostics.(grpcplugin.DiagnosticsClient); ok {
|
||||
@@ -82,12 +89,24 @@ func newClientV2(descriptor PluginDescriptor, logger log.Logger, rpcClient plugi
|
||||
}
|
||||
}
|
||||
|
||||
if rawSecretsManager != nil {
|
||||
if secretsManagerPlugin, ok := rawSecretsManager.(secretsmanagerplugin.SecretsManagerPlugin); ok {
|
||||
c.SecretsManagerPlugin = secretsManagerPlugin
|
||||
}
|
||||
}
|
||||
|
||||
if descriptor.startRendererFn != nil {
|
||||
if err := descriptor.startRendererFn(descriptor.pluginID, c.RendererPlugin, logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if descriptor.startSecretsManagerFn != nil {
|
||||
if err := descriptor.startSecretsManagerFn(descriptor.pluginID, c.SecretsManagerPlugin, logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/grpcplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||
)
|
||||
|
||||
// PluginBackendProvider is a function type for initializing a Plugin backend.
|
||||
@@ -24,7 +25,7 @@ type Service struct {
|
||||
|
||||
func New(providers ...PluginBackendProvider) *Service {
|
||||
if len(providers) == 0 {
|
||||
return New(RendererProvider, DefaultProvider)
|
||||
return New(RendererProvider, SecretsManagerProvider, DefaultProvider)
|
||||
}
|
||||
return &Service{
|
||||
providerChain: providers,
|
||||
@@ -32,7 +33,7 @@ func New(providers ...PluginBackendProvider) *Service {
|
||||
}
|
||||
|
||||
func ProvideService(coreRegistry *coreplugin.Registry) *Service {
|
||||
return New(coreRegistry.BackendFactoryProvider(), RendererProvider, DefaultProvider)
|
||||
return New(coreRegistry.BackendFactoryProvider(), RendererProvider, SecretsManagerProvider, DefaultProvider)
|
||||
}
|
||||
|
||||
func (s *Service) BackendFactory(ctx context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
|
||||
@@ -56,6 +57,18 @@ var RendererProvider PluginBackendProvider = func(_ context.Context, p *plugins.
|
||||
)
|
||||
}
|
||||
|
||||
var SecretsManagerProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
|
||||
if !p.IsSecretsManager() {
|
||||
return nil
|
||||
}
|
||||
return grpcplugin.NewSecretsManagerPlugin(p.ID, filepath.Join(p.PluginDir, secretsManagerStartCmd()),
|
||||
func(pluginID string, secretsmanager secretsmanagerplugin.SecretsManagerPlugin, logger log.Logger) error {
|
||||
p.SecretsManager = secretsmanager
|
||||
return nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var DefaultProvider PluginBackendProvider = func(_ context.Context, p *plugins.Plugin) backendplugin.PluginFactoryFunc {
|
||||
// TODO check for executable
|
||||
return grpcplugin.NewBackendPlugin(p.ID, filepath.Join(p.PluginDir, pluginStartCmd(p.Executable)))
|
||||
@@ -84,3 +97,15 @@ func rendererStartCmd() string {
|
||||
|
||||
return fmt.Sprintf("%s_%s_%s%s", "plugin_start", os, strings.ToLower(arch), extension)
|
||||
}
|
||||
|
||||
func secretsManagerStartCmd() string {
|
||||
os := strings.ToLower(runtime.GOOS)
|
||||
arch := runtime.GOARCH
|
||||
extension := ""
|
||||
|
||||
if os == "windows" {
|
||||
extension = ".exe"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s_%s_%s%s", "secrets_plugin_start", os, strings.ToLower(arch), extension)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
protoc --go_out=. --go_opt=paths=source_relative \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
secretsmanager.proto
|
||||
@@ -0,0 +1,786 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.19.4
|
||||
// source: secretsmanager.proto
|
||||
|
||||
package secretsmanagerplugin
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type SecretsGetRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SecretsGetRequest) Reset() {
|
||||
*x = SecretsGetRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SecretsGetRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SecretsGetRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SecretsGetRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SecretsGetRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SecretsGetRequest) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *SecretsGetRequest) GetKeyDescriptor() *Key {
|
||||
if x != nil {
|
||||
return x.KeyDescriptor
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SecretsSetRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SecretsSetRequest) Reset() {
|
||||
*x = SecretsSetRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SecretsSetRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SecretsSetRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SecretsSetRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SecretsSetRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SecretsSetRequest) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *SecretsSetRequest) GetKeyDescriptor() *Key {
|
||||
if x != nil {
|
||||
return x.KeyDescriptor
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SecretsSetRequest) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SecretsDelRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SecretsDelRequest) Reset() {
|
||||
*x = SecretsDelRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SecretsDelRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SecretsDelRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SecretsDelRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SecretsDelRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SecretsDelRequest) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *SecretsDelRequest) GetKeyDescriptor() *Key {
|
||||
if x != nil {
|
||||
return x.KeyDescriptor
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SecretsKeysRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
AllOrganizations bool `protobuf:"varint,2,opt,name=allOrganizations,proto3" json:"allOrganizations,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SecretsKeysRequest) Reset() {
|
||||
*x = SecretsKeysRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SecretsKeysRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SecretsKeysRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SecretsKeysRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SecretsKeysRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SecretsKeysRequest) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *SecretsKeysRequest) GetKeyDescriptor() *Key {
|
||||
if x != nil {
|
||||
return x.KeyDescriptor
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SecretsKeysRequest) GetAllOrganizations() bool {
|
||||
if x != nil {
|
||||
return x.AllOrganizations
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type SecretsRenameRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
|
||||
NewNamespace string `protobuf:"bytes,4,opt,name=newNamespace,proto3" json:"newNamespace,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SecretsRenameRequest) Reset() {
|
||||
*x = SecretsRenameRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SecretsRenameRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SecretsRenameRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SecretsRenameRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SecretsRenameRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SecretsRenameRequest) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *SecretsRenameRequest) GetKeyDescriptor() *Key {
|
||||
if x != nil {
|
||||
return x.KeyDescriptor
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SecretsRenameRequest) GetNewNamespace() string {
|
||||
if x != nil {
|
||||
return x.NewNamespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Key struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
OrgId int64 `protobuf:"varint,1,opt,name=orgId,proto3" json:"orgId,omitempty"`
|
||||
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
|
||||
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Key) Reset() {
|
||||
*x = Key{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Key) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Key) ProtoMessage() {}
|
||||
|
||||
func (x *Key) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Key.ProtoReflect.Descriptor instead.
|
||||
func (*Key) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *Key) GetOrgId() int64 {
|
||||
if x != nil {
|
||||
return x.OrgId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Key) GetNamespace() string {
|
||||
if x != nil {
|
||||
return x.Namespace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Key) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SecretsErrorResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SecretsErrorResponse) Reset() {
|
||||
*x = SecretsErrorResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SecretsErrorResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SecretsErrorResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SecretsErrorResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[6]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SecretsErrorResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SecretsErrorResponse) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *SecretsErrorResponse) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SecretsGetResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
DecryptedValue string `protobuf:"bytes,2,opt,name=decryptedValue,proto3" json:"decryptedValue,omitempty"`
|
||||
Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SecretsGetResponse) Reset() {
|
||||
*x = SecretsGetResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SecretsGetResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SecretsGetResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SecretsGetResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[7]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SecretsGetResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SecretsGetResponse) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *SecretsGetResponse) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SecretsGetResponse) GetDecryptedValue() string {
|
||||
if x != nil {
|
||||
return x.DecryptedValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SecretsGetResponse) GetExists() bool {
|
||||
if x != nil {
|
||||
return x.Exists
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type SecretsKeysResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
|
||||
Keys []*Key `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SecretsKeysResponse) Reset() {
|
||||
*x = SecretsKeysResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_secretsmanager_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SecretsKeysResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SecretsKeysResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SecretsKeysResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_secretsmanager_proto_msgTypes[8]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SecretsKeysResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SecretsKeysResponse) Descriptor() ([]byte, []int) {
|
||||
return file_secretsmanager_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *SecretsKeysResponse) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SecretsKeysResponse) GetKeys() []*Key {
|
||||
if x != nil {
|
||||
return x.Keys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_secretsmanager_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_secretsmanager_proto_rawDesc = []byte{
|
||||
0x0a, 0x14, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d,
|
||||
0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 0x54, 0x0a, 0x11,
|
||||
0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
|
||||
0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65,
|
||||
0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e,
|
||||
0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
|
||||
0x6f, 0x72, 0x22, 0x6a, 0x0a, 0x11, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x53, 0x65, 0x74,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65,
|
||||
0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19,
|
||||
0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70,
|
||||
0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65,
|
||||
0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x54,
|
||||
0x0a, 0x11, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x44, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69,
|
||||
0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63,
|
||||
0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69,
|
||||
0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69,
|
||||
0x70, 0x74, 0x6f, 0x72, 0x22, 0x81, 0x01, 0x0a, 0x12, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73,
|
||||
0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0d, 0x6b,
|
||||
0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61,
|
||||
0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x0d, 0x6b,
|
||||
0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x2a, 0x0a, 0x10,
|
||||
0x61, 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x4f, 0x72, 0x67, 0x61, 0x6e,
|
||||
0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x7b, 0x0a, 0x14, 0x53, 0x65, 0x63, 0x72,
|
||||
0x65, 0x74, 0x73, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x12, 0x3f, 0x0a, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f,
|
||||
0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
|
||||
0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x4b,
|
||||
0x65, 0x79, 0x52, 0x0d, 0x6b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f,
|
||||
0x72, 0x12, 0x22, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63,
|
||||
0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65,
|
||||
0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x4d, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x6f, 0x72, 0x67, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6f, 0x72, 0x67,
|
||||
0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x22, 0x2c, 0x0a, 0x14, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45,
|
||||
0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72,
|
||||
0x6f, 0x72, 0x22, 0x6a, 0x0a, 0x12, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x47, 0x65, 0x74,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f,
|
||||
0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x26,
|
||||
0x0a, 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65,
|
||||
0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x78, 0x69, 0x73, 0x74, 0x73, 0x22, 0x5a,
|
||||
0x0a, 0x13, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2d, 0x0a, 0x04, 0x6b,
|
||||
0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x73, 0x65, 0x63, 0x72,
|
||||
0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x2e, 0x4b, 0x65, 0x79, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x32, 0xe7, 0x03, 0x0a, 0x14, 0x52,
|
||||
0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x4d, 0x61, 0x6e, 0x61,
|
||||
0x67, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x27, 0x2e, 0x73, 0x65, 0x63,
|
||||
0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69,
|
||||
0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e,
|
||||
0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65,
|
||||
0x74, 0x73, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a,
|
||||
0x03, 0x53, 0x65, 0x74, 0x12, 0x27, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61,
|
||||
0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72,
|
||||
0x65, 0x74, 0x73, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e,
|
||||
0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c,
|
||||
0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x72, 0x72, 0x6f,
|
||||
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x03, 0x44, 0x65, 0x6c,
|
||||
0x12, 0x27, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
|
||||
0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x44,
|
||||
0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x73, 0x65, 0x63, 0x72,
|
||||
0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x04, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x28, 0x2e,
|
||||
0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c,
|
||||
0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x4b, 0x65, 0x79, 0x73,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
|
||||
0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x53,
|
||||
0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x60, 0x0a, 0x06, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x2e, 0x73,
|
||||
0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75,
|
||||
0x67, 0x69, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x6e, 0x61, 0x6d,
|
||||
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x73, 0x65, 0x63, 0x72, 0x65,
|
||||
0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e,
|
||||
0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x42, 0x19, 0x5a, 0x17, 0x2e, 0x2f, 0x3b, 0x73, 0x65, 0x63, 0x72, 0x65,
|
||||
0x74, 0x73, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_secretsmanager_proto_rawDescOnce sync.Once
|
||||
file_secretsmanager_proto_rawDescData = file_secretsmanager_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_secretsmanager_proto_rawDescGZIP() []byte {
|
||||
file_secretsmanager_proto_rawDescOnce.Do(func() {
|
||||
file_secretsmanager_proto_rawDescData = protoimpl.X.CompressGZIP(file_secretsmanager_proto_rawDescData)
|
||||
})
|
||||
return file_secretsmanager_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_secretsmanager_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_secretsmanager_proto_goTypes = []interface{}{
|
||||
(*SecretsGetRequest)(nil), // 0: secretsmanagerplugin.SecretsGetRequest
|
||||
(*SecretsSetRequest)(nil), // 1: secretsmanagerplugin.SecretsSetRequest
|
||||
(*SecretsDelRequest)(nil), // 2: secretsmanagerplugin.SecretsDelRequest
|
||||
(*SecretsKeysRequest)(nil), // 3: secretsmanagerplugin.SecretsKeysRequest
|
||||
(*SecretsRenameRequest)(nil), // 4: secretsmanagerplugin.SecretsRenameRequest
|
||||
(*Key)(nil), // 5: secretsmanagerplugin.Key
|
||||
(*SecretsErrorResponse)(nil), // 6: secretsmanagerplugin.SecretsErrorResponse
|
||||
(*SecretsGetResponse)(nil), // 7: secretsmanagerplugin.SecretsGetResponse
|
||||
(*SecretsKeysResponse)(nil), // 8: secretsmanagerplugin.SecretsKeysResponse
|
||||
}
|
||||
var file_secretsmanager_proto_depIdxs = []int32{
|
||||
5, // 0: secretsmanagerplugin.SecretsGetRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
|
||||
5, // 1: secretsmanagerplugin.SecretsSetRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
|
||||
5, // 2: secretsmanagerplugin.SecretsDelRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
|
||||
5, // 3: secretsmanagerplugin.SecretsKeysRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
|
||||
5, // 4: secretsmanagerplugin.SecretsRenameRequest.keyDescriptor:type_name -> secretsmanagerplugin.Key
|
||||
5, // 5: secretsmanagerplugin.SecretsKeysResponse.keys:type_name -> secretsmanagerplugin.Key
|
||||
0, // 6: secretsmanagerplugin.RemoteSecretsManager.Get:input_type -> secretsmanagerplugin.SecretsGetRequest
|
||||
1, // 7: secretsmanagerplugin.RemoteSecretsManager.Set:input_type -> secretsmanagerplugin.SecretsSetRequest
|
||||
2, // 8: secretsmanagerplugin.RemoteSecretsManager.Del:input_type -> secretsmanagerplugin.SecretsDelRequest
|
||||
3, // 9: secretsmanagerplugin.RemoteSecretsManager.Keys:input_type -> secretsmanagerplugin.SecretsKeysRequest
|
||||
4, // 10: secretsmanagerplugin.RemoteSecretsManager.Rename:input_type -> secretsmanagerplugin.SecretsRenameRequest
|
||||
7, // 11: secretsmanagerplugin.RemoteSecretsManager.Get:output_type -> secretsmanagerplugin.SecretsGetResponse
|
||||
6, // 12: secretsmanagerplugin.RemoteSecretsManager.Set:output_type -> secretsmanagerplugin.SecretsErrorResponse
|
||||
6, // 13: secretsmanagerplugin.RemoteSecretsManager.Del:output_type -> secretsmanagerplugin.SecretsErrorResponse
|
||||
8, // 14: secretsmanagerplugin.RemoteSecretsManager.Keys:output_type -> secretsmanagerplugin.SecretsKeysResponse
|
||||
6, // 15: secretsmanagerplugin.RemoteSecretsManager.Rename:output_type -> secretsmanagerplugin.SecretsErrorResponse
|
||||
11, // [11:16] is the sub-list for method output_type
|
||||
6, // [6:11] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_secretsmanager_proto_init() }
|
||||
func file_secretsmanager_proto_init() {
|
||||
if File_secretsmanager_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_secretsmanager_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SecretsGetRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_secretsmanager_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SecretsSetRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_secretsmanager_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SecretsDelRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_secretsmanager_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SecretsKeysRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_secretsmanager_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SecretsRenameRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_secretsmanager_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Key); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_secretsmanager_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SecretsErrorResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_secretsmanager_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SecretsGetResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_secretsmanager_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SecretsKeysResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_secretsmanager_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_secretsmanager_proto_goTypes,
|
||||
DependencyIndexes: file_secretsmanager_proto_depIdxs,
|
||||
MessageInfos: file_secretsmanager_proto_msgTypes,
|
||||
}.Build()
|
||||
File_secretsmanager_proto = out.File
|
||||
file_secretsmanager_proto_rawDesc = nil
|
||||
file_secretsmanager_proto_goTypes = nil
|
||||
file_secretsmanager_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
syntax = "proto3";
|
||||
package secretsmanagerplugin;
|
||||
|
||||
option go_package = "./;secretsmanagerplugin";
|
||||
|
||||
message SecretsGetRequest {
|
||||
Key keyDescriptor = 1;
|
||||
}
|
||||
|
||||
message SecretsSetRequest {
|
||||
Key keyDescriptor = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message SecretsDelRequest {
|
||||
Key keyDescriptor = 1;
|
||||
}
|
||||
|
||||
message SecretsKeysRequest {
|
||||
Key keyDescriptor = 1;
|
||||
bool allOrganizations = 2;
|
||||
}
|
||||
|
||||
message SecretsRenameRequest {
|
||||
Key keyDescriptor = 1;
|
||||
string newNamespace = 2;
|
||||
}
|
||||
|
||||
message Key {
|
||||
int64 orgId = 1;
|
||||
string namespace = 2;
|
||||
string type = 3;
|
||||
}
|
||||
|
||||
message SecretsErrorResponse {
|
||||
string error = 1;
|
||||
}
|
||||
|
||||
message SecretsGetResponse {
|
||||
string error = 1;
|
||||
string decryptedValue = 2;
|
||||
bool exists = 3;
|
||||
}
|
||||
|
||||
message SecretsKeysResponse {
|
||||
string error = 1;
|
||||
repeated Key keys = 2;
|
||||
}
|
||||
|
||||
service RemoteSecretsManager {
|
||||
rpc Get(SecretsGetRequest) returns (SecretsGetResponse);
|
||||
rpc Set(SecretsSetRequest) returns (SecretsErrorResponse);
|
||||
rpc Del(SecretsDelRequest) returns (SecretsErrorResponse);
|
||||
rpc Keys(SecretsKeysRequest) returns (SecretsKeysResponse);
|
||||
rpc Rename(SecretsRenameRequest) returns (SecretsErrorResponse);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package secretsmanagerplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type SecretsManagerPlugin interface {
|
||||
RemoteSecretsManagerClient
|
||||
}
|
||||
|
||||
type SecretsManagerGRPCPlugin struct {
|
||||
plugin.NetRPCUnsupportedPlugin
|
||||
}
|
||||
|
||||
func (p *SecretsManagerGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SecretsManagerGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
|
||||
return &SecretsManagerGRPCClient{NewRemoteSecretsManagerClient(c)}, nil
|
||||
}
|
||||
|
||||
type SecretsManagerGRPCClient struct {
|
||||
RemoteSecretsManagerClient
|
||||
}
|
||||
|
||||
// Get an item from the store
|
||||
func (sm *SecretsManagerGRPCClient) Get(ctx context.Context, req *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error) {
|
||||
return sm.RemoteSecretsManagerClient.Get(ctx, req)
|
||||
}
|
||||
|
||||
// Set an item in the store
|
||||
func (sm *SecretsManagerGRPCClient) Set(ctx context.Context, req *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
|
||||
return sm.RemoteSecretsManagerClient.Set(ctx, req)
|
||||
}
|
||||
|
||||
// Del deletes an item from the store.
|
||||
func (sm *SecretsManagerGRPCClient) Del(ctx context.Context, req *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
|
||||
return sm.RemoteSecretsManagerClient.Del(ctx, req)
|
||||
}
|
||||
|
||||
// Keys get all keys for a given namespace.
|
||||
func (sm *SecretsManagerGRPCClient) Keys(ctx context.Context, req *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error) {
|
||||
return sm.RemoteSecretsManagerClient.Keys(ctx, req)
|
||||
}
|
||||
|
||||
// Rename an item in the store
|
||||
func (sm *SecretsManagerGRPCClient) Rename(ctx context.Context, req *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
|
||||
return sm.RemoteSecretsManagerClient.Rename(ctx, req)
|
||||
}
|
||||
|
||||
var _ RemoteSecretsManagerClient = &SecretsManagerGRPCClient{}
|
||||
var _ plugin.GRPCPlugin = &SecretsManagerGRPCPlugin{}
|
||||
@@ -0,0 +1,249 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.19.4
|
||||
// source: secretsmanager.proto
|
||||
|
||||
package secretsmanagerplugin
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// RemoteSecretsManagerClient is the client API for RemoteSecretsManager service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type RemoteSecretsManagerClient interface {
|
||||
Get(ctx context.Context, in *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error)
|
||||
Set(ctx context.Context, in *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
|
||||
Del(ctx context.Context, in *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
|
||||
Keys(ctx context.Context, in *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error)
|
||||
Rename(ctx context.Context, in *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error)
|
||||
}
|
||||
|
||||
type remoteSecretsManagerClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewRemoteSecretsManagerClient(cc grpc.ClientConnInterface) RemoteSecretsManagerClient {
|
||||
return &remoteSecretsManagerClient{cc}
|
||||
}
|
||||
|
||||
func (c *remoteSecretsManagerClient) Get(ctx context.Context, in *SecretsGetRequest, opts ...grpc.CallOption) (*SecretsGetResponse, error) {
|
||||
out := new(SecretsGetResponse)
|
||||
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Get", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *remoteSecretsManagerClient) Set(ctx context.Context, in *SecretsSetRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
|
||||
out := new(SecretsErrorResponse)
|
||||
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Set", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *remoteSecretsManagerClient) Del(ctx context.Context, in *SecretsDelRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
|
||||
out := new(SecretsErrorResponse)
|
||||
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Del", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *remoteSecretsManagerClient) Keys(ctx context.Context, in *SecretsKeysRequest, opts ...grpc.CallOption) (*SecretsKeysResponse, error) {
|
||||
out := new(SecretsKeysResponse)
|
||||
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Keys", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *remoteSecretsManagerClient) Rename(ctx context.Context, in *SecretsRenameRequest, opts ...grpc.CallOption) (*SecretsErrorResponse, error) {
|
||||
out := new(SecretsErrorResponse)
|
||||
err := c.cc.Invoke(ctx, "/secretsmanagerplugin.RemoteSecretsManager/Rename", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RemoteSecretsManagerServer is the server API for RemoteSecretsManager service.
|
||||
// All implementations must embed UnimplementedRemoteSecretsManagerServer
|
||||
// for forward compatibility
|
||||
type RemoteSecretsManagerServer interface {
|
||||
Get(context.Context, *SecretsGetRequest) (*SecretsGetResponse, error)
|
||||
Set(context.Context, *SecretsSetRequest) (*SecretsErrorResponse, error)
|
||||
Del(context.Context, *SecretsDelRequest) (*SecretsErrorResponse, error)
|
||||
Keys(context.Context, *SecretsKeysRequest) (*SecretsKeysResponse, error)
|
||||
Rename(context.Context, *SecretsRenameRequest) (*SecretsErrorResponse, error)
|
||||
mustEmbedUnimplementedRemoteSecretsManagerServer()
|
||||
}
|
||||
|
||||
// UnimplementedRemoteSecretsManagerServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedRemoteSecretsManagerServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedRemoteSecretsManagerServer) Get(context.Context, *SecretsGetRequest) (*SecretsGetResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
|
||||
}
|
||||
func (UnimplementedRemoteSecretsManagerServer) Set(context.Context, *SecretsSetRequest) (*SecretsErrorResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Set not implemented")
|
||||
}
|
||||
func (UnimplementedRemoteSecretsManagerServer) Del(context.Context, *SecretsDelRequest) (*SecretsErrorResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Del not implemented")
|
||||
}
|
||||
func (UnimplementedRemoteSecretsManagerServer) Keys(context.Context, *SecretsKeysRequest) (*SecretsKeysResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Keys not implemented")
|
||||
}
|
||||
func (UnimplementedRemoteSecretsManagerServer) Rename(context.Context, *SecretsRenameRequest) (*SecretsErrorResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Rename not implemented")
|
||||
}
|
||||
func (UnimplementedRemoteSecretsManagerServer) mustEmbedUnimplementedRemoteSecretsManagerServer() {}
|
||||
|
||||
// UnsafeRemoteSecretsManagerServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to RemoteSecretsManagerServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeRemoteSecretsManagerServer interface {
|
||||
mustEmbedUnimplementedRemoteSecretsManagerServer()
|
||||
}
|
||||
|
||||
func RegisterRemoteSecretsManagerServer(s grpc.ServiceRegistrar, srv RemoteSecretsManagerServer) {
|
||||
s.RegisterService(&RemoteSecretsManager_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _RemoteSecretsManager_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SecretsGetRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RemoteSecretsManagerServer).Get(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Get",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RemoteSecretsManagerServer).Get(ctx, req.(*SecretsGetRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RemoteSecretsManager_Set_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SecretsSetRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RemoteSecretsManagerServer).Set(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Set",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RemoteSecretsManagerServer).Set(ctx, req.(*SecretsSetRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RemoteSecretsManager_Del_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SecretsDelRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RemoteSecretsManagerServer).Del(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Del",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RemoteSecretsManagerServer).Del(ctx, req.(*SecretsDelRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RemoteSecretsManager_Keys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SecretsKeysRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RemoteSecretsManagerServer).Keys(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Keys",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RemoteSecretsManagerServer).Keys(ctx, req.(*SecretsKeysRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RemoteSecretsManager_Rename_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SecretsRenameRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RemoteSecretsManagerServer).Rename(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/secretsmanagerplugin.RemoteSecretsManager/Rename",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RemoteSecretsManagerServer).Rename(ctx, req.(*SecretsRenameRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// RemoteSecretsManager_ServiceDesc is the grpc.ServiceDesc for RemoteSecretsManager service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var RemoteSecretsManager_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "secretsmanagerplugin.RemoteSecretsManager",
|
||||
HandlerType: (*RemoteSecretsManagerServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Get",
|
||||
Handler: _RemoteSecretsManager_Get_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Set",
|
||||
Handler: _RemoteSecretsManager_Set_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Del",
|
||||
Handler: _RemoteSecretsManager_Del_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Keys",
|
||||
Handler: _RemoteSecretsManager_Keys_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Rename",
|
||||
Handler: _RemoteSecretsManager_Rename_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "secretsmanager.proto",
|
||||
}
|
||||
@@ -47,6 +47,11 @@ type RendererManager interface {
|
||||
Renderer() *Plugin
|
||||
}
|
||||
|
||||
type SecretsPluginManager interface {
|
||||
// SecretsManager returns a secretsmanager plugin
|
||||
SecretsManager() *Plugin
|
||||
}
|
||||
|
||||
type StaticRouteResolver interface {
|
||||
Routes() []*StaticRoute
|
||||
}
|
||||
|
||||
@@ -79,6 +79,36 @@ func TestInitializer_Initialize(t *testing.T) {
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("secretsmanager", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "test",
|
||||
Type: plugins.SecretsManager,
|
||||
Dependencies: plugins.Dependencies{
|
||||
GrafanaVersion: ">=8.x",
|
||||
},
|
||||
Backend: true,
|
||||
},
|
||||
PluginDir: absCurPath,
|
||||
Class: plugins.External,
|
||||
}
|
||||
|
||||
i := &Initializer{
|
||||
cfg: plugins.NewCfg(),
|
||||
log: log.NewNopLogger(),
|
||||
backendProvider: &fakeBackendProvider{
|
||||
plugin: p,
|
||||
},
|
||||
}
|
||||
|
||||
err := i.Initialize(context.Background(), p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
c, exists := p.Client()
|
||||
assert.True(t, exists)
|
||||
assert.NotNil(t, c)
|
||||
})
|
||||
|
||||
t.Run("non backend plugin app", func(t *testing.T) {
|
||||
p := &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
|
||||
@@ -24,6 +24,7 @@ var _ plugins.Client = (*PluginManager)(nil)
|
||||
var _ plugins.Store = (*PluginManager)(nil)
|
||||
var _ plugins.StaticRouteResolver = (*PluginManager)(nil)
|
||||
var _ plugins.RendererManager = (*PluginManager)(nil)
|
||||
var _ plugins.SecretsPluginManager = (*PluginManager)(nil)
|
||||
|
||||
type PluginManager struct {
|
||||
cfg *plugins.Cfg
|
||||
@@ -117,6 +118,16 @@ func (m *PluginManager) Renderer() *plugins.Plugin {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) SecretsManager() *plugins.Plugin {
|
||||
for _, p := range m.availablePlugins(context.TODO()) {
|
||||
if p.IsSecretsManager() {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PluginManager) Routes() []*plugins.StaticRoute {
|
||||
staticRoutes := make([]*plugins.StaticRoute, 0)
|
||||
|
||||
|
||||
+17
-9
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/pluginextensionv2"
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
@@ -36,9 +37,10 @@ type Plugin struct {
|
||||
Module string
|
||||
BaseURL string
|
||||
|
||||
Renderer pluginextensionv2.RendererPlugin
|
||||
client backendplugin.Plugin
|
||||
log log.Logger
|
||||
Renderer pluginextensionv2.RendererPlugin
|
||||
SecretsManager secretsmanagerplugin.SecretsManagerPlugin
|
||||
client backendplugin.Plugin
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
type PluginDTO struct {
|
||||
@@ -132,7 +134,7 @@ type JSONData struct {
|
||||
Streaming bool `json:"streaming"`
|
||||
SDK bool `json:"sdk,omitempty"`
|
||||
|
||||
// Backend (Datasource + Renderer)
|
||||
// Backend (Datasource + Renderer + SecretsManager)
|
||||
Executable string `json:"executable,omitempty"`
|
||||
}
|
||||
|
||||
@@ -347,6 +349,10 @@ func (p *Plugin) IsRenderer() bool {
|
||||
return p.Type == "renderer"
|
||||
}
|
||||
|
||||
func (p *Plugin) IsSecretsManager() bool {
|
||||
return p.Type == "secretsmanager"
|
||||
}
|
||||
|
||||
func (p *Plugin) IsDataSource() bool {
|
||||
return p.Type == "datasource"
|
||||
}
|
||||
@@ -384,20 +390,22 @@ var PluginTypes = []Type{
|
||||
Panel,
|
||||
App,
|
||||
Renderer,
|
||||
SecretsManager,
|
||||
}
|
||||
|
||||
type Type string
|
||||
|
||||
const (
|
||||
DataSource Type = "datasource"
|
||||
Panel Type = "panel"
|
||||
App Type = "app"
|
||||
Renderer Type = "renderer"
|
||||
DataSource Type = "datasource"
|
||||
Panel Type = "panel"
|
||||
App Type = "app"
|
||||
Renderer Type = "renderer"
|
||||
SecretsManager Type = "secretsmanager"
|
||||
)
|
||||
|
||||
func (pt Type) IsValid() bool {
|
||||
switch pt {
|
||||
case DataSource, Panel, App, Renderer:
|
||||
case DataSource, Panel, App, Renderer, SecretsManager:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -149,6 +149,7 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(plugins.DashboardFileStore), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.StaticRouteResolver), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.RendererManager), new(*manager.PluginManager)),
|
||||
wire.Bind(new(plugins.SecretsPluginManager), new(*manager.PluginManager)),
|
||||
coreplugin.ProvideCoreRegistry,
|
||||
loader.ProvideService,
|
||||
wire.Bind(new(loader.Service), new(*loader.Loader)),
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers"
|
||||
"github.com/grafana/grafana/pkg/services/searchusers/filters"
|
||||
secretsStore "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
|
||||
"github.com/grafana/grafana/pkg/services/thumbs"
|
||||
"github.com/grafana/grafana/pkg/services/validations"
|
||||
@@ -85,6 +86,8 @@ var wireExtsBasicSet = wire.NewSet(
|
||||
wire.Bind(new(registry.UsageStatsProvidersRegistry), new(*usagestatssvcs.UsageStatsProvidersRegistry)),
|
||||
ossaccesscontrol.ProvideDatasourcePermissionsService,
|
||||
wire.Bind(new(accesscontrol.DatasourcePermissionsService), new(*ossaccesscontrol.DatasourcePermissionsService)),
|
||||
secretsStore.ProvideRemotePluginCheck,
|
||||
wire.Bind(new(secretsStore.UseRemoteSecretsPluginCheck), new(*secretsStore.OSSRemoteSecretsPluginCheck)),
|
||||
)
|
||||
|
||||
var wireExtsSet = wire.NewSet(
|
||||
|
||||
@@ -13,7 +13,7 @@ var (
|
||||
)
|
||||
|
||||
type DashboardVersion struct {
|
||||
ID int64 `json:"id"`
|
||||
ID int64 `json:"id" xorm:"pk autoincr 'id'"`
|
||||
DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"`
|
||||
ParentVersion int `json:"parentVersion"`
|
||||
RestoredFrom int `json:"restoredFrom"`
|
||||
@@ -45,9 +45,9 @@ type ListDashboardVersionsQuery struct {
|
||||
}
|
||||
|
||||
type DashboardVersionDTO struct {
|
||||
ID int64 `json:"id"`
|
||||
DashboardID int64 `json:"dashboardId"`
|
||||
DashboardUID string `json:"dashboardUid"`
|
||||
ID int64 `json:"id" xorm:"id"`
|
||||
DashboardID int64 `json:"dashboardId" xorm:"dashboard_id"`
|
||||
DashboardUID string `json:"dashboardUid" xorm:"dashboard_uid"`
|
||||
ParentVersion int `json:"parentVersion"`
|
||||
RestoredFrom int `json:"restoredFrom"`
|
||||
Version int `json:"version"`
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// pkg/services/featuremgmt/registry.go
|
||||
// Then run tests in:
|
||||
// pkg/services/featuremgmt/toggles_gen_test.go
|
||||
// twice to generate and validate the feature flag files
|
||||
|
||||
package featuremgmt
|
||||
|
||||
@@ -72,6 +73,12 @@ var (
|
||||
State: FeatureStateBeta,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "tempoApmTable",
|
||||
Description: "Show APM table",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "prometheus_azure_auth",
|
||||
Description: "Experimental. Azure authentication for Prometheus datasource",
|
||||
@@ -192,11 +199,10 @@ var (
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "azureMonitorExperimentalUI",
|
||||
Description: "Use grafana-experimental UI in Azure Monitor",
|
||||
State: FeatureStateAlpha,
|
||||
RequiresDevMode: true,
|
||||
FrontendOnly: true,
|
||||
Name: "azureMonitorExperimentalUI",
|
||||
Description: "Use grafana-experimental UI in Azure Monitor",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "traceToMetrics",
|
||||
@@ -226,5 +232,25 @@ var (
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "cloudMonitoringExperimentalUI",
|
||||
Description: "Use grafana-experimental UI in Cloud Monitoring",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "logRequestsInstrumentedAsUnknown",
|
||||
Description: "Logs the path for requests that are instrumented as unknown",
|
||||
},
|
||||
{
|
||||
Name: "dataConnectionsConsole",
|
||||
Description: "Enables a new top-level page called Data Connections. This page is an experiment for better grouping of installing / configuring data sources and other plugins.",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
{
|
||||
Name: "internationalization",
|
||||
Description: "Enables work-in-progress internationalization",
|
||||
State: FeatureStateAlpha,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -55,6 +55,10 @@ const (
|
||||
// show service
|
||||
FlagTempoServiceGraph = "tempoServiceGraph"
|
||||
|
||||
// FlagTempoApmTable
|
||||
// Show APM table
|
||||
FlagTempoApmTable = "tempoApmTable"
|
||||
|
||||
// FlagPrometheusAzureAuth
|
||||
// Experimental. Azure authentication for Prometheus datasource
|
||||
FlagPrometheusAzureAuth = "prometheus_azure_auth"
|
||||
@@ -166,4 +170,20 @@ const (
|
||||
// FlagCanvasPanelNesting
|
||||
// Allow elements nesting
|
||||
FlagCanvasPanelNesting = "canvasPanelNesting"
|
||||
|
||||
// FlagCloudMonitoringExperimentalUI
|
||||
// Use grafana-experimental UI in Cloud Monitoring
|
||||
FlagCloudMonitoringExperimentalUI = "cloudMonitoringExperimentalUI"
|
||||
|
||||
// FlagLogRequestsInstrumentedAsUnknown
|
||||
// Logs the path for requests that are instrumented as unknown
|
||||
FlagLogRequestsInstrumentedAsUnknown = "logRequestsInstrumentedAsUnknown"
|
||||
|
||||
// FlagDataConnectionsConsole
|
||||
// Enables a new top-level page called Data Connections. This page is an experiment for better grouping of installing / configuring data sources and other plugins.
|
||||
FlagDataConnectionsConsole = "dataConnectionsConsole"
|
||||
|
||||
// FlagInternationalization
|
||||
// Enables work-in-progress internationalization
|
||||
FlagInternationalization = "internationalization"
|
||||
)
|
||||
|
||||
@@ -143,11 +143,11 @@ func (a *AccessControlDashboardGuardian) CanCreate(folderID int64, isFolder bool
|
||||
func (a *AccessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) {
|
||||
ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator)
|
||||
if err != nil {
|
||||
a.log.Error("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserId, "id", a.dashboardID)
|
||||
a.log.Debug("Failed to evaluate access control to folder or dashboard", "error", err, "userId", a.user.UserId, "id", a.dashboardID)
|
||||
}
|
||||
|
||||
if !ok && err == nil {
|
||||
a.log.Info("Access denied to folder or dashboard", "userId", a.user.UserId, "id", a.dashboardID, "permissions", evaluator.GoString())
|
||||
a.log.Debug("Access denied to folder or dashboard", "userId", a.user.UserId, "id", a.dashboardID, "permissions", evaluator.GoString())
|
||||
}
|
||||
|
||||
return ok, err
|
||||
|
||||
@@ -14,9 +14,9 @@ var (
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.rules:reader",
|
||||
DisplayName: "Rules Reader",
|
||||
Description: "Can read alert rules in all Grafana folders and external providers",
|
||||
Description: "Read alert rules in all Grafana folders and external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 2,
|
||||
Version: 3,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingRuleRead,
|
||||
@@ -30,13 +30,13 @@ var (
|
||||
},
|
||||
}
|
||||
|
||||
rulesEditorRole = accesscontrol.RoleRegistration{
|
||||
rulesWriterRole = accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.rules:editor",
|
||||
DisplayName: "Rules Editor",
|
||||
Description: "Can add, update, and delete rules in any Grafana folder and external providers",
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.rules:writer",
|
||||
DisplayName: "Rules Writer",
|
||||
Description: "Add, update, and delete rules in any Grafana folder and external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 3,
|
||||
Version: 4,
|
||||
Permissions: accesscontrol.ConcatPermissions(rulesReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingRuleCreate,
|
||||
@@ -62,9 +62,9 @@ var (
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.instances:reader",
|
||||
DisplayName: "Instances and Silences Reader",
|
||||
Description: "Can read instances and silences of Grafana and external providers",
|
||||
Description: "Read instances and silences of Grafana and external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 1,
|
||||
Version: 2,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingInstanceRead,
|
||||
@@ -78,13 +78,13 @@ var (
|
||||
},
|
||||
}
|
||||
|
||||
instancesEditorRole = accesscontrol.RoleRegistration{
|
||||
instancesWriterRole = accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.instances:editor",
|
||||
DisplayName: "Silences Editor",
|
||||
Description: "Can add and update silences in Grafana and external providers",
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.instances:writer",
|
||||
DisplayName: "Silences Writer",
|
||||
Description: "Add and update silences in Grafana and external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 2,
|
||||
Version: 3,
|
||||
Permissions: accesscontrol.ConcatPermissions(instancesReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingInstanceCreate,
|
||||
@@ -104,9 +104,9 @@ var (
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.notifications:reader",
|
||||
DisplayName: "Notifications Reader",
|
||||
Description: "Can read notification policies and contact points in Grafana and external providers",
|
||||
Description: "Read notification policies and contact points in Grafana and external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 1,
|
||||
Version: 2,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingNotificationsRead,
|
||||
@@ -119,13 +119,13 @@ var (
|
||||
},
|
||||
}
|
||||
|
||||
notificationsEditorRole = accesscontrol.RoleRegistration{
|
||||
notificationsWriterRole = accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.notifications:editor",
|
||||
DisplayName: "Notifications Editor",
|
||||
Description: "Can add, update, and delete contact points and notification policies in Grafana and external providers",
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.notifications:writer",
|
||||
DisplayName: "Notifications Writer",
|
||||
Description: "Add, update, and delete contact points and notification policies in Grafana and external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 2,
|
||||
Version: 3,
|
||||
Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingNotificationsWrite,
|
||||
@@ -142,9 +142,9 @@ var (
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting:reader",
|
||||
DisplayName: "Full read-only access",
|
||||
Description: "Can read alert rules, instances, silences, contact points, and notification policies in Grafana and all external providers",
|
||||
Description: "Read alert rules, instances, silences, contact points, and notification policies in Grafana and all external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 2,
|
||||
Version: 3,
|
||||
Permissions: accesscontrol.ConcatPermissions(rulesReaderRole.Role.Permissions, instancesReaderRole.Role.Permissions, notificationsReaderRole.Role.Permissions),
|
||||
},
|
||||
Grants: []string{string(models.ROLE_VIEWER)},
|
||||
@@ -152,23 +152,23 @@ var (
|
||||
|
||||
alertingWriterRole = accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting:editor",
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting:writer",
|
||||
DisplayName: "Full access",
|
||||
Description: "Can add,update and delete alert rules, instances, silences, contact points, and notification policies in Grafana and all external providers",
|
||||
Description: "Add,update and delete alert rules, instances, silences, contact points, and notification policies in Grafana and all external providers",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 3,
|
||||
Permissions: accesscontrol.ConcatPermissions(rulesEditorRole.Role.Permissions, instancesEditorRole.Role.Permissions, notificationsEditorRole.Role.Permissions),
|
||||
Version: 4,
|
||||
Permissions: accesscontrol.ConcatPermissions(rulesWriterRole.Role.Permissions, instancesWriterRole.Role.Permissions, notificationsWriterRole.Role.Permissions),
|
||||
},
|
||||
Grants: []string{string(models.ROLE_EDITOR), string(models.ROLE_ADMIN)},
|
||||
}
|
||||
|
||||
alertingProvisioningRole = accesscontrol.RoleRegistration{
|
||||
alertingProvisionerRole = accesscontrol.RoleRegistration{
|
||||
Role: accesscontrol.RoleDTO{
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting:provisioning",
|
||||
Name: accesscontrol.FixedRolePrefix + "alerting.provisioning:writer",
|
||||
DisplayName: "Access to alert rules provisioning API",
|
||||
Description: "Can manage all alert rules, contact points, notification policies, silences, etc. in the organization via provisioning API.",
|
||||
Description: "Manage all alert rules, contact points, notification policies, silences, etc. in the organization via provisioning API.",
|
||||
Group: AlertRolesGroup,
|
||||
Version: 1,
|
||||
Version: 2,
|
||||
Permissions: []accesscontrol.Permission{
|
||||
{
|
||||
Action: accesscontrol.ActionAlertingProvisioningRead, // organization scope
|
||||
@@ -184,9 +184,9 @@ var (
|
||||
|
||||
func DeclareFixedRoles(ac accesscontrol.AccessControl) error {
|
||||
return ac.DeclareFixedRoles(
|
||||
rulesReaderRole, rulesEditorRole,
|
||||
instancesReaderRole, instancesEditorRole,
|
||||
notificationsReaderRole, notificationsEditorRole,
|
||||
alertingReaderRole, alertingWriterRole, alertingProvisioningRole,
|
||||
rulesReaderRole, rulesWriterRole,
|
||||
instancesReaderRole, instancesWriterRole,
|
||||
notificationsReaderRole, notificationsWriterRole,
|
||||
alertingReaderRole, alertingWriterRole, alertingProvisionerRole,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -136,6 +138,13 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser
|
||||
Queries: []backend.DataQuery{},
|
||||
}
|
||||
|
||||
middlewares := []httpclient.Middleware{}
|
||||
if parsedReq.httpRequest != nil {
|
||||
middlewares = append(middlewares,
|
||||
httpclientprovider.ForwardedCookiesMiddleware(parsedReq.httpRequest.Cookies(), ds.AllowedCookies()),
|
||||
)
|
||||
}
|
||||
|
||||
if s.oAuthTokenService.IsOAuthPassThruEnabled(ds) {
|
||||
if token := s.oAuthTokenService.GetCurrentOAuthToken(ctx, user); token != nil {
|
||||
req.Headers["Authorization"] = fmt.Sprintf("%s %s", token.Type(), token.AccessToken)
|
||||
@@ -144,6 +153,7 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser
|
||||
if ok && idToken != "" {
|
||||
req.Headers["X-ID-Token"] = idToken
|
||||
}
|
||||
middlewares = append(middlewares, httpclientprovider.ForwardedOAuthIdentityMiddleware(token))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +172,8 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser
|
||||
req.Queries = append(req.Queries, q.query)
|
||||
}
|
||||
|
||||
ctx = httpclient.WithContextualMiddleware(ctx, middlewares...)
|
||||
|
||||
return s.pluginClient.QueryData(ctx, req)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,26 @@ const (
|
||||
AllOrganizations = -1
|
||||
)
|
||||
|
||||
func ProvideService(sqlStore sqlstore.Store, secretsService secrets.Service) SecretsKVStore {
|
||||
func ProvideService(sqlStore sqlstore.Store, secretsService secrets.Service, remoteCheck UseRemoteSecretsPluginCheck) SecretsKVStore {
|
||||
logger := log.New("secrets.kvstore")
|
||||
if remoteCheck.ShouldUseRemoteSecretsPlugin() {
|
||||
logger.Debug("secrets kvstore is using a remote plugin for secrets management")
|
||||
secretsPlugin, err := remoteCheck.GetPlugin()
|
||||
if err != nil {
|
||||
logger.Error("plugin client was nil, falling back to SQL implementation")
|
||||
} else {
|
||||
return &secretsKVStorePlugin{
|
||||
secretsPlugin: secretsPlugin,
|
||||
secretsService: secretsService,
|
||||
log: logger,
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Debug("secrets kvstore is using the default (SQL) implementation for secrets management")
|
||||
return &secretsKVStoreSQL{
|
||||
sqlStore: sqlStore,
|
||||
secretsService: secretsService,
|
||||
log: log.New("secrets.kvstore"),
|
||||
log: logger,
|
||||
decryptionCache: decryptionCache{
|
||||
cache: make(map[int64]cachedDecrypted),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package kvstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
smp "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
)
|
||||
|
||||
// secretsKVStorePlugin provides a key/value store backed by the Grafana plugin gRPC interface
|
||||
type secretsKVStorePlugin struct {
|
||||
log log.Logger
|
||||
secretsPlugin smp.SecretsManagerPlugin
|
||||
secretsService secrets.Service
|
||||
}
|
||||
|
||||
// Get an item from the store
|
||||
func (kv *secretsKVStorePlugin) Get(ctx context.Context, orgId int64, namespace string, typ string) (string, bool, error) {
|
||||
req := &smp.SecretsGetRequest{
|
||||
KeyDescriptor: &smp.Key{
|
||||
OrgId: orgId,
|
||||
Namespace: namespace,
|
||||
Type: typ,
|
||||
},
|
||||
}
|
||||
res, err := kv.secretsPlugin.Get(ctx, req)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
} else if res.Error != "" {
|
||||
err = fmt.Errorf(res.Error)
|
||||
}
|
||||
|
||||
return res.DecryptedValue, res.Exists, err
|
||||
}
|
||||
|
||||
// Set an item in the store
|
||||
func (kv *secretsKVStorePlugin) Set(ctx context.Context, orgId int64, namespace string, typ string, value string) error {
|
||||
req := &smp.SecretsSetRequest{
|
||||
KeyDescriptor: &smp.Key{
|
||||
OrgId: orgId,
|
||||
Namespace: namespace,
|
||||
Type: typ,
|
||||
},
|
||||
Value: value,
|
||||
}
|
||||
|
||||
res, err := kv.secretsPlugin.Set(ctx, req)
|
||||
if err == nil && res.Error != "" {
|
||||
err = fmt.Errorf(res.Error)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Del deletes an item from the store.
|
||||
func (kv *secretsKVStorePlugin) Del(ctx context.Context, orgId int64, namespace string, typ string) error {
|
||||
req := &smp.SecretsDelRequest{
|
||||
KeyDescriptor: &smp.Key{
|
||||
OrgId: orgId,
|
||||
Namespace: namespace,
|
||||
Type: typ,
|
||||
},
|
||||
}
|
||||
|
||||
res, err := kv.secretsPlugin.Del(ctx, req)
|
||||
if err == nil && res.Error != "" {
|
||||
err = fmt.Errorf(res.Error)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Keys get all keys for a given namespace. To query for all
|
||||
// organizations the constant 'kvstore.AllOrganizations' can be passed as orgId.
|
||||
func (kv *secretsKVStorePlugin) Keys(ctx context.Context, orgId int64, namespace string, typ string) ([]Key, error) {
|
||||
req := &smp.SecretsKeysRequest{
|
||||
KeyDescriptor: &smp.Key{
|
||||
OrgId: orgId,
|
||||
Namespace: namespace,
|
||||
Type: typ,
|
||||
},
|
||||
AllOrganizations: orgId == AllOrganizations,
|
||||
}
|
||||
|
||||
res, err := kv.secretsPlugin.Keys(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if res.Error != "" {
|
||||
err = fmt.Errorf(res.Error)
|
||||
}
|
||||
|
||||
return parseKeys(res.Keys), err
|
||||
}
|
||||
|
||||
// Rename an item in the store
|
||||
func (kv *secretsKVStorePlugin) Rename(ctx context.Context, orgId int64, namespace string, typ string, newNamespace string) error {
|
||||
req := &smp.SecretsRenameRequest{
|
||||
KeyDescriptor: &smp.Key{
|
||||
OrgId: orgId,
|
||||
Namespace: namespace,
|
||||
Type: typ,
|
||||
},
|
||||
NewNamespace: newNamespace,
|
||||
}
|
||||
|
||||
res, err := kv.secretsPlugin.Rename(ctx, req)
|
||||
if err == nil && res.Error != "" {
|
||||
err = fmt.Errorf(res.Error)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func parseKeys(keys []*smp.Key) []Key {
|
||||
var newKeys []Key
|
||||
|
||||
for _, k := range keys {
|
||||
newKey := Key{OrgId: k.OrgId, Namespace: k.Namespace, Type: k.Type}
|
||||
newKeys = append(newKeys, newKey)
|
||||
}
|
||||
|
||||
return newKeys
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package kvstore
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin"
|
||||
)
|
||||
|
||||
type UseRemoteSecretsPluginCheck interface {
|
||||
ShouldUseRemoteSecretsPlugin() bool
|
||||
GetPlugin() (secretsmanagerplugin.SecretsManagerPlugin, error)
|
||||
}
|
||||
|
||||
type OSSRemoteSecretsPluginCheck struct {
|
||||
UseRemoteSecretsPluginCheck
|
||||
}
|
||||
|
||||
func ProvideRemotePluginCheck() *OSSRemoteSecretsPluginCheck {
|
||||
return &OSSRemoteSecretsPluginCheck{}
|
||||
}
|
||||
|
||||
func (c *OSSRemoteSecretsPluginCheck) ShouldUseRemoteSecretsPlugin() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *OSSRemoteSecretsPluginCheck) GetPlugin() (secretsmanagerplugin.SecretsManagerPlugin, error) {
|
||||
return nil, nil
|
||||
}
|
||||
+110
-8
@@ -489,7 +489,7 @@ func TestAlertingEnabled(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when legacy alerting is enabled and unified is invalid (or not defined) [OSS]",
|
||||
desc: "when legacy alerting is enabled and unified is not defined [OSS]",
|
||||
legacyAlertingEnabled: "true",
|
||||
unifiedAlertingEnabled: "",
|
||||
isEnterprise: false,
|
||||
@@ -507,7 +507,24 @@ func TestAlertingEnabled(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when legacy alerting is enabled and unified is invalid (or not defined) [Enterprise]",
|
||||
desc: "when legacy alerting is enabled and unified is invalid [OSS]",
|
||||
legacyAlertingEnabled: "true",
|
||||
unifiedAlertingEnabled: "invalid",
|
||||
isEnterprise: false,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.readFeatureToggles(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.ReadUnifiedAlertingSettings(f)
|
||||
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
|
||||
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
|
||||
assert.NotNil(t, AlertingEnabled)
|
||||
assert.Equal(t, false, *AlertingEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when legacy alerting is enabled and unified is not defined [Enterprise]",
|
||||
legacyAlertingEnabled: "true",
|
||||
unifiedAlertingEnabled: "",
|
||||
isEnterprise: true,
|
||||
@@ -525,10 +542,27 @@ func TestAlertingEnabled(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when legacy alerting is disabled and unified is invalid (or not defined) [OSS]",
|
||||
legacyAlertingEnabled: "false",
|
||||
desc: "when legacy alerting is enabled and unified is invalid [Enterprise]",
|
||||
legacyAlertingEnabled: "true",
|
||||
unifiedAlertingEnabled: "invalid",
|
||||
isEnterprise: false,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.readFeatureToggles(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.ReadUnifiedAlertingSettings(f)
|
||||
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
|
||||
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
|
||||
assert.NotNil(t, AlertingEnabled)
|
||||
assert.Equal(t, false, *AlertingEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when legacy alerting is disabled and unified is not defined [OSS]",
|
||||
legacyAlertingEnabled: "false",
|
||||
unifiedAlertingEnabled: "",
|
||||
isEnterprise: false,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
require.NoError(t, err)
|
||||
@@ -543,9 +577,26 @@ func TestAlertingEnabled(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when legacy alerting is disabled and unified is invalid (or not defined) [Enterprise]",
|
||||
desc: "when legacy alerting is disabled and unified is invalid [OSS]",
|
||||
legacyAlertingEnabled: "false",
|
||||
unifiedAlertingEnabled: "invalid",
|
||||
isEnterprise: false,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.readFeatureToggles(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.ReadUnifiedAlertingSettings(f)
|
||||
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
|
||||
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
|
||||
assert.NotNil(t, AlertingEnabled)
|
||||
assert.Equal(t, false, *AlertingEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when legacy alerting is disabled and unified is not defined [Enterprise]",
|
||||
legacyAlertingEnabled: "false",
|
||||
unifiedAlertingEnabled: "",
|
||||
isEnterprise: true,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
@@ -561,10 +612,27 @@ func TestAlertingEnabled(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when both are invalid (or not defined) [OSS]",
|
||||
legacyAlertingEnabled: "invalid",
|
||||
desc: "when legacy alerting is disabled and unified is invalid [Enterprise]",
|
||||
legacyAlertingEnabled: "false",
|
||||
unifiedAlertingEnabled: "invalid",
|
||||
isEnterprise: false,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.readFeatureToggles(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.ReadUnifiedAlertingSettings(f)
|
||||
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
|
||||
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
|
||||
assert.NotNil(t, AlertingEnabled)
|
||||
assert.Equal(t, false, *AlertingEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when both are not defined [OSS]",
|
||||
legacyAlertingEnabled: "",
|
||||
unifiedAlertingEnabled: "",
|
||||
isEnterprise: false,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
require.NoError(t, err)
|
||||
@@ -578,9 +646,26 @@ func TestAlertingEnabled(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when both are invalid (or not defined) [Enterprise]",
|
||||
desc: "when both are not invalid [OSS]",
|
||||
legacyAlertingEnabled: "invalid",
|
||||
unifiedAlertingEnabled: "invalid",
|
||||
isEnterprise: false,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.readFeatureToggles(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.ReadUnifiedAlertingSettings(f)
|
||||
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
|
||||
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
|
||||
assert.NotNil(t, AlertingEnabled)
|
||||
assert.Equal(t, false, *AlertingEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when both are not defined [Enterprise]",
|
||||
legacyAlertingEnabled: "",
|
||||
unifiedAlertingEnabled: "",
|
||||
isEnterprise: true,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
@@ -594,6 +679,23 @@ func TestAlertingEnabled(t *testing.T) {
|
||||
assert.Nil(t, AlertingEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when both are not invalid [Enterprise]",
|
||||
legacyAlertingEnabled: "invalid",
|
||||
unifiedAlertingEnabled: "invalid",
|
||||
isEnterprise: false,
|
||||
verifyCfg: func(t *testing.T, cfg Cfg, f *ini.File) {
|
||||
err := readAlertingSettings(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.readFeatureToggles(f)
|
||||
require.NoError(t, err)
|
||||
err = cfg.ReadUnifiedAlertingSettings(f)
|
||||
assert.EqualError(t, err, "failed to read unified alerting enabled setting: invalid value invalid, should be either true or false")
|
||||
assert.Nil(t, cfg.UnifiedAlerting.Enabled)
|
||||
assert.NotNil(t, AlertingEnabled)
|
||||
assert.Equal(t, false, *AlertingEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "when both are false",
|
||||
legacyAlertingEnabled: "false",
|
||||
|
||||
@@ -103,31 +103,39 @@ func (cfg *Cfg) readUnifiedAlertingEnabledSetting(section *ini.Section) (*bool,
|
||||
// At present an invalid value is considered the same as no value. This means that a
|
||||
// spelling mistake in the string "false" could enable unified alerting rather
|
||||
// than disable it. This issue can be found here
|
||||
unifiedAlerting, err := section.Key("enabled").Bool()
|
||||
if err != nil {
|
||||
hasEnabled := section.Key("enabled").Value() != ""
|
||||
if !hasEnabled {
|
||||
// TODO: Remove in Grafana v9
|
||||
if cfg.IsFeatureToggleEnabled("ngalert") {
|
||||
cfg.Logger.Warn("ngalert feature flag is deprecated: use unified alerting enabled setting instead")
|
||||
// feature flag overrides the legacy alerting setting
|
||||
legacyAlerting := false
|
||||
AlertingEnabled = &legacyAlerting
|
||||
unifiedAlerting = true
|
||||
unifiedAlerting := true
|
||||
return &unifiedAlerting, nil
|
||||
}
|
||||
|
||||
// if legacy alerting has not been configured then enable unified alerting
|
||||
if AlertingEnabled == nil {
|
||||
unifiedAlerting = true
|
||||
unifiedAlerting := true
|
||||
return &unifiedAlerting, nil
|
||||
}
|
||||
|
||||
// enable unified alerting and disable legacy alerting
|
||||
legacyAlerting := false
|
||||
AlertingEnabled = &legacyAlerting
|
||||
unifiedAlerting = true
|
||||
unifiedAlerting := true
|
||||
return &unifiedAlerting, nil
|
||||
}
|
||||
|
||||
unifiedAlerting, err := section.Key("enabled").Bool()
|
||||
if err != nil {
|
||||
// the value for unified alerting is invalid so disable all alerting
|
||||
legacyAlerting := false
|
||||
AlertingEnabled = &legacyAlerting
|
||||
return nil, fmt.Errorf("invalid value %s, should be either true or false", section.Key("enabled"))
|
||||
}
|
||||
|
||||
// If both legacy and unified alerting are enabled then return an error
|
||||
if AlertingEnabled != nil && *AlertingEnabled && unifiedAlerting {
|
||||
return nil, errors.New("legacy and unified alerting cannot both be enabled at the same time, please disable one of them and restart Grafana")
|
||||
@@ -149,7 +157,7 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error {
|
||||
ua := iniFile.Section("unified_alerting")
|
||||
uaCfg.Enabled, err = cfg.readUnifiedAlertingEnabledSetting(ua)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to read unified alerting enabled setting: %w", err)
|
||||
}
|
||||
|
||||
uaCfg.DisabledOrgs = make(map[int64]struct{})
|
||||
|
||||
@@ -205,7 +205,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
zIndex: theme.zIndex.sidemenu,
|
||||
padding: `${theme.spacing(1)} 0`,
|
||||
position: 'relative',
|
||||
width: `calc(${theme.spacing(7)} + 1px)`,
|
||||
width: theme.components.sidemenu.width,
|
||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||
|
||||
[theme.breakpoints.down('md')]: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { DataSourceApi, formattedValueToString, getValueFormat, PanelData, PanelPlugin } from '@grafana/data';
|
||||
import { CoreApp, DataSourceApi, formattedValueToString, getValueFormat, PanelData, PanelPlugin } from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { Drawer, Tab, TabsBar } from '@grafana/ui';
|
||||
import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
|
||||
@@ -90,6 +90,7 @@ export const InspectContent: React.FC<Props> = ({
|
||||
options={dataOptions}
|
||||
onOptionsChange={onDataOptionsChange}
|
||||
timeZone={dashboard.timezone}
|
||||
app={CoreApp.Dashboard}
|
||||
/>
|
||||
)}
|
||||
{data && activeTab === InspectTab.Meta && (
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { render, RenderResult, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import DataConnectionsPage from './DataConnectionsPage';
|
||||
import navIndex from './__mocks__/store.navIndex.mock';
|
||||
import { ROUTE_BASE_ID } from './constants';
|
||||
|
||||
const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => {
|
||||
// @ts-ignore
|
||||
const store = configureStore({ navIndex });
|
||||
locationService.push(path);
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<DataConnectionsPage />
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Data Connections Page', () => {
|
||||
test('shows all the four tabs', async () => {
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByLabelText('Tab Data sources')).toBeVisible();
|
||||
expect(await screen.findByLabelText('Tab Plugins')).toBeVisible();
|
||||
expect(await screen.findByLabelText('Tab Cloud integrations')).toBeVisible();
|
||||
expect(await screen.findByLabelText('Tab Recorded queries')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows the "Data sources" tab by default', async () => {
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('The list of data sources is under development.')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { ROUTES } from './constants';
|
||||
import { useNavModel } from './hooks/useNavModel';
|
||||
import { CloudIntegrations } from './tabs/CloudIntegrations';
|
||||
import { DataSources } from './tabs/DataSources';
|
||||
import { Plugins } from './tabs/Plugins';
|
||||
import { RecordedQueries } from './tabs/RecordedQueries';
|
||||
|
||||
export default function DataConnectionsPage(): React.ReactElement | null {
|
||||
const navModel = useNavModel();
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<Switch>
|
||||
<Route exact path={ROUTES.Plugins} component={Plugins} />
|
||||
<Route exact path={ROUTES.CloudIntegrations} component={CloudIntegrations} />
|
||||
<Route exact path={ROUTES.RecordedQueries} component={RecordedQueries} />
|
||||
|
||||
{/* Default page */}
|
||||
<Route component={DataSources} />
|
||||
</Switch>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
// The ID of the app plugin that we render under that "Cloud Integrations" tab
|
||||
export const CLOUD_ONBOARDING_APP_ID = 'grafana-easystart-app';
|
||||
|
||||
// The ID of the main nav-tree item (the main item in the NavIndex)
|
||||
export const ROUTE_BASE_ID = 'data-connections';
|
||||
|
||||
export enum ROUTES {
|
||||
DataSources = '/data-connections/data-sources',
|
||||
Plugins = '/data-connections/plugins',
|
||||
CloudIntegrations = '/data-connections/cloud-integrations',
|
||||
RecordedQueries = '/data-connections/recorded-queries',
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
import { ROUTE_BASE_ID } from '../constants';
|
||||
|
||||
// We need this utility logic to make sure that the tab with the current URL is marked as active.
|
||||
// (In case we were using `getNavModel()` from app/core/selectors/navModel, then we would need to set
|
||||
// the child nav-model-item's ID on the call-site.)
|
||||
export const useNavModel = () => {
|
||||
const { pathname } = useLocation();
|
||||
const navIndex = useSelector((state: StoreState) => state.navIndex);
|
||||
const node = navIndex[ROUTE_BASE_ID];
|
||||
const main = node;
|
||||
|
||||
main.children = main.children?.map((item) => ({
|
||||
...item,
|
||||
active: item.url === pathname,
|
||||
}));
|
||||
|
||||
return {
|
||||
node,
|
||||
main,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
||||
import { config } from 'app/core/config';
|
||||
import { RouteDescriptor } from 'app/core/navigation/types';
|
||||
|
||||
import { ROUTE_BASE_ID } from './constants';
|
||||
|
||||
export function getRoutes(): RouteDescriptor[] {
|
||||
if (config.featureToggles.dataConnectionsConsole) {
|
||||
return [
|
||||
{
|
||||
path: `/${ROUTE_BASE_ID}`,
|
||||
exact: false,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DataConnectionsPage"*/ 'app/features/data-connections/DataConnectionsPage')
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { AppPluginLoader } from 'app/features/plugins/components/AppPluginLoader';
|
||||
|
||||
import { CLOUD_ONBOARDING_APP_ID } from '../../constants';
|
||||
|
||||
export function CloudIntegrations(): ReactElement | null {
|
||||
const s = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={s.container}>
|
||||
<AppPluginLoader id={CLOUD_ONBOARDING_APP_ID} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
// We would like to force the app to stay inside the provided tab
|
||||
container: css`
|
||||
position: relative;
|
||||
`,
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export * from './CloudIntegrations';
|
||||
@@ -0,0 +1,5 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
export function DataSources(): ReactElement | null {
|
||||
return <div>The list of data sources is under development.</div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './DataSources';
|
||||
@@ -0,0 +1,5 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
export function Plugins(): ReactElement | null {
|
||||
return <div>The list of plugins is under development</div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Plugins';
|
||||
@@ -0,0 +1,5 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
export function RecordedQueries(): ReactElement | null {
|
||||
return <div>The recorded queries tab is under development.</div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './RecordedQueries';
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { TimeZone } from '@grafana/data';
|
||||
import { CoreApp, TimeZone } from '@grafana/data';
|
||||
import { TabbedContainer, TabConfig } from '@grafana/ui';
|
||||
import { ExploreDrawer } from 'app/features/explore/ExploreDrawer';
|
||||
import { InspectDataTab } from 'app/features/inspector/InspectDataTab';
|
||||
@@ -51,6 +51,7 @@ export function ExploreQueryInspector(props: Props) {
|
||||
isLoading={loading}
|
||||
options={{ withTransforms: false, withFieldConfig: false }}
|
||||
timeZone={timeZone}
|
||||
app={CoreApp.Explore}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { lazy, PureComponent, RefObject, Suspense } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { DataSourceInstanceSettings, RawTimeRange } from '@grafana/data';
|
||||
import { config, DataSourcePicker } from '@grafana/runtime';
|
||||
import { config, DataSourcePicker, reportInteraction } from '@grafana/runtime';
|
||||
import {
|
||||
defaultIntervals,
|
||||
PageToolbar,
|
||||
@@ -192,17 +192,28 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
|
||||
|
||||
{hasLiveOption && (
|
||||
<LiveTailControls exploreId={exploreId}>
|
||||
{(controls) => (
|
||||
<LiveTailButton
|
||||
splitted={splitted}
|
||||
isLive={isLive}
|
||||
isPaused={isPaused}
|
||||
start={controls.start}
|
||||
pause={controls.pause}
|
||||
resume={controls.resume}
|
||||
stop={controls.stop}
|
||||
/>
|
||||
)}
|
||||
{(c) => {
|
||||
const controls = {
|
||||
...c,
|
||||
start: () => {
|
||||
reportInteraction('grafana_explore_logs_result_displayed', {
|
||||
datasourceType: this.props.datasourceType,
|
||||
});
|
||||
c.start();
|
||||
},
|
||||
};
|
||||
return (
|
||||
<LiveTailButton
|
||||
splitted={splitted}
|
||||
isLive={isLive}
|
||||
isPaused={isPaused}
|
||||
start={controls.start}
|
||||
pause={controls.pause}
|
||||
resume={controls.resume}
|
||||
stop={controls.stop}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</LiveTailControls>
|
||||
)}
|
||||
</ToolbarButtonRow>
|
||||
@@ -223,6 +234,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps) => {
|
||||
return {
|
||||
datasourceMissing,
|
||||
datasourceName: datasourceInstance?.name,
|
||||
datasourceType: datasourceInstance?.type,
|
||||
loading,
|
||||
range,
|
||||
timeZone: getTimeZone(state.user),
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
SplitOpen,
|
||||
DataQueryResponse,
|
||||
} from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { TooltipDisplayMode } from '@grafana/schema';
|
||||
import {
|
||||
RadioButtonGroup,
|
||||
@@ -68,6 +69,7 @@ interface Props extends Themeable2 {
|
||||
scanning?: boolean;
|
||||
scanRange?: RawTimeRange;
|
||||
exploreId: ExploreId;
|
||||
datasourceType?: string;
|
||||
logsVolumeData: DataQueryResponse | undefined;
|
||||
loadLogsVolumeData: (exploreId: ExploreId) => void;
|
||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||
@@ -144,6 +146,10 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
onChangeDedup = (dedupStrategy: LogsDedupStrategy) => {
|
||||
reportInteraction('grafana_explore_logs_deduplication_clicked', {
|
||||
deduplicationType: dedupStrategy,
|
||||
datasourceType: this.props.datasourceType,
|
||||
});
|
||||
this.setState({ dedupStrategy });
|
||||
};
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
<Collapse label="Logs" loading={loading} isOpen className={styleOverridesForStickyNavigation}>
|
||||
<Logs
|
||||
exploreId={exploreId}
|
||||
datasourceType={this.props.datasourceInstance?.type}
|
||||
logRows={logRows}
|
||||
logsMeta={logsMeta}
|
||||
logsSeries={logsSeries}
|
||||
|
||||
@@ -5,6 +5,12 @@ import { LogsSortOrder } from '@grafana/data';
|
||||
|
||||
import LogsNavigation from './LogsNavigation';
|
||||
|
||||
// we have to mock out reportInteraction, otherwise it crashes the test.
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
reportInteraction: () => null,
|
||||
}));
|
||||
|
||||
type LogsNavigationProps = ComponentProps<typeof LogsNavigation>;
|
||||
const defaultProps: LogsNavigationProps = {
|
||||
absoluteRange: { from: 1637319381811, to: 1637322981811 },
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isEqual } from 'lodash';
|
||||
import React, { memo, useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { LogsSortOrder, AbsoluteTimeRange, TimeZone, DataQuery, GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { LogsNavigationPages } from './LogsNavigationPages';
|
||||
@@ -107,6 +108,9 @@ function LogsNavigation({
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
//If we are not on the last page, use next page's range
|
||||
reportInteraction('grafana_explore_logs_pagination_clicked', {
|
||||
pageType: 'olderLogsButton',
|
||||
});
|
||||
if (!onLastPage) {
|
||||
const indexChange = oldestLogsFirst ? -1 : 1;
|
||||
changeTime({
|
||||
@@ -133,6 +137,9 @@ function LogsNavigation({
|
||||
className={styles.navButton}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
reportInteraction('grafana_explore_logs_pagination_clicked', {
|
||||
pageType: 'newerLogsButton',
|
||||
});
|
||||
//If we are not on the first page, use previous page's range
|
||||
if (!onFirstPage) {
|
||||
const indexChange = oldestLogsFirst ? 1 : -1;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { dateTimeFormat, systemDateFormats, TimeZone, AbsoluteTimeRange, GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { CustomScrollbar, Spinner, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { LogsPage } from './LogsNavigation';
|
||||
@@ -51,7 +52,13 @@ export function LogsNavigationPages({
|
||||
data-testid={`page${index + 1}`}
|
||||
className={styles.page}
|
||||
key={page.queryRange.to}
|
||||
onClick={() => !loading && changeTime({ from: page.queryRange.from, to: page.queryRange.to })}
|
||||
onClick={() => {
|
||||
reportInteraction('grafana_explore_logs_pagination_clicked', {
|
||||
pageType: 'page',
|
||||
pageNumber: index + 1,
|
||||
});
|
||||
!loading && changeTime({ from: page.queryRange.from, to: page.queryRange.to });
|
||||
}}
|
||||
>
|
||||
<div className={cx(styles.line, { selectedBg: currentPageIndex === index })} />
|
||||
<div className={cx(styles.time, { selectedText: currentPageIndex === index })}>
|
||||
|
||||
@@ -118,7 +118,6 @@ describe('Explore: Query History', () => {
|
||||
await openQueryHistory();
|
||||
await assertQueryHistoryExists(RAW_QUERY);
|
||||
|
||||
expect(reportInteractionMock).toBeCalledTimes(2);
|
||||
expect(reportInteractionMock).toBeCalledWith('grafana_explore_query_history_opened', {
|
||||
queryHistoryEnabled: false,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
QueryFixAction,
|
||||
toLegacyResponseData,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import {
|
||||
buildQueryTransaction,
|
||||
ensureQueries,
|
||||
@@ -442,6 +442,11 @@ export const runQueries = (
|
||||
)
|
||||
.subscribe({
|
||||
next(data) {
|
||||
if (data.logsResult !== null) {
|
||||
reportInteraction('grafana_explore_logs_result_displayed', {
|
||||
datasourceType: datasourceInstance.type,
|
||||
});
|
||||
}
|
||||
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
|
||||
|
||||
// Keep scanning for results if this was the last scanning transaction
|
||||
|
||||
@@ -16,8 +16,10 @@ import {
|
||||
toCSV,
|
||||
transformDataFrame,
|
||||
TimeZone,
|
||||
CoreApp,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Spinner, Table } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { dataFrameToLogsModel } from 'app/core/logs_model';
|
||||
@@ -34,6 +36,7 @@ interface Props {
|
||||
isLoading: boolean;
|
||||
options: GetDataOptions;
|
||||
timeZone: TimeZone;
|
||||
app?: CoreApp;
|
||||
data?: DataFrame[];
|
||||
panel?: PanelModel;
|
||||
onOptionsChange?: (options: GetDataOptions) => void;
|
||||
@@ -107,7 +110,11 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
exportLogsAsTxt = () => {
|
||||
const { data, panel } = this.props;
|
||||
const { data, panel, app } = this.props;
|
||||
reportInteraction('grafana_logs_download_logs_clicked', {
|
||||
app,
|
||||
format: 'logs',
|
||||
});
|
||||
const logsModel = dataFrameToLogsModel(data || [], undefined);
|
||||
let textToDownload = '';
|
||||
|
||||
@@ -223,7 +230,7 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading, options, data, panel, onOptionsChange } = this.props;
|
||||
const { isLoading, options, data, panel, onOptionsChange, app } = this.props;
|
||||
const { dataFrameIndex, transformId, transformationOptions, selectedDataFrame, downloadForExcel } = this.state;
|
||||
const styles = getPanelInspectorStyles();
|
||||
|
||||
@@ -266,7 +273,15 @@ export class InspectDataTab extends PureComponent<Props, State> {
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel })}
|
||||
onClick={() => {
|
||||
if (hasLogs) {
|
||||
reportInteraction('grafana_logs_download_clicked', {
|
||||
app,
|
||||
format: 'csv',
|
||||
});
|
||||
}
|
||||
this.exportCsv(dataFrames[dataFrameIndex], { useExcelHeader: this.state.downloadForExcel });
|
||||
}}
|
||||
className={css`
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import OrgProfile, { Props } from './OrgProfile';
|
||||
@@ -11,19 +12,36 @@ jest.mock('app/core/core', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const setup = () => {
|
||||
describe('OrgProfile', () => {
|
||||
const props: Props = {
|
||||
orgName: 'Main org',
|
||||
onSubmit: jest.fn(),
|
||||
};
|
||||
|
||||
return shallow(<OrgProfile {...props} />);
|
||||
};
|
||||
it('should render without crashing', () => {
|
||||
expect(() => render(<OrgProfile {...props} />)).not.toThrow();
|
||||
});
|
||||
|
||||
describe('Render', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
it('should show the current org name', () => {
|
||||
render(<OrgProfile {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const orgNameInput = screen.getByLabelText('Organization name');
|
||||
|
||||
expect(orgNameInput).toHaveValue('Main org');
|
||||
});
|
||||
|
||||
it('can update the current org name', async () => {
|
||||
render(<OrgProfile {...props} />);
|
||||
|
||||
const orgNameInput = screen.getByLabelText('Organization name');
|
||||
const submitButton = screen.getByRole('button', { name: 'Update organization name' });
|
||||
|
||||
expect(orgNameInput).toHaveValue('Main org');
|
||||
|
||||
await userEvent.clear(orgNameInput);
|
||||
await userEvent.type(orgNameInput, 'New org name');
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('New org name');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<Form
|
||||
defaultValues={
|
||||
Object {
|
||||
"orgName": "Main org",
|
||||
}
|
||||
}
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<Component />
|
||||
</Form>
|
||||
`;
|
||||
@@ -0,0 +1,132 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React, { Component } from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
|
||||
import { locationService, setEchoSrv } from '@grafana/runtime';
|
||||
import { Echo } from 'app/core/services/echo/Echo';
|
||||
|
||||
import { getMockPlugin } from '../__mocks__/pluginMocks';
|
||||
import { useImportAppPlugin } from '../hooks/useImportAppPlugin';
|
||||
|
||||
import { AppPluginLoader } from './AppPluginLoader';
|
||||
|
||||
jest.mock('../hooks/useImportAppPlugin', () => ({
|
||||
useImportAppPlugin: jest.fn(),
|
||||
}));
|
||||
|
||||
const useImportAppPluginMock = useImportAppPlugin as jest.Mock<
|
||||
ReturnType<typeof useImportAppPlugin>,
|
||||
Parameters<typeof useImportAppPlugin>
|
||||
>;
|
||||
|
||||
const TEXTS = {
|
||||
PLUGIN_TITLE: 'Amazing App',
|
||||
PLUGIN_CONTENT: 'This is my amazing app plugin!',
|
||||
PLUGIN_TAB_TITLE_A: 'Tab (A)',
|
||||
PLUGIN_TAB_TITLE_B: 'Tab (B)',
|
||||
};
|
||||
|
||||
describe('AppPluginLoader', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
AppPluginComponent.timesMounted = 0;
|
||||
setEchoSrv(new Echo());
|
||||
});
|
||||
|
||||
test('renders the app plugin correctly', async () => {
|
||||
useImportAppPluginMock.mockReturnValue({ value: getAppPluginMock(), loading: false, error: undefined });
|
||||
|
||||
renderAppPlugin();
|
||||
|
||||
expect(await screen.findByText(TEXTS.PLUGIN_TITLE)).toBeVisible();
|
||||
expect(await screen.findByText(TEXTS.PLUGIN_CONTENT)).toBeVisible();
|
||||
expect(await screen.findByLabelText(`Tab ${TEXTS.PLUGIN_TAB_TITLE_A}`)).toBeVisible();
|
||||
expect(await screen.findByLabelText(`Tab ${TEXTS.PLUGIN_TAB_TITLE_B}`)).toBeVisible();
|
||||
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the app plugin only once', async () => {
|
||||
useImportAppPluginMock.mockReturnValue({ value: getAppPluginMock(), loading: false, error: undefined });
|
||||
|
||||
renderAppPlugin();
|
||||
|
||||
expect(await screen.findByText(TEXTS.PLUGIN_TITLE)).toBeVisible();
|
||||
expect(AppPluginComponent.timesMounted).toEqual(1);
|
||||
});
|
||||
|
||||
test('renders a loader while the plugin is loading', async () => {
|
||||
useImportAppPluginMock.mockReturnValue({ value: undefined, loading: true, error: undefined });
|
||||
|
||||
renderAppPlugin();
|
||||
|
||||
expect(await screen.findByText('Loading ...')).toBeVisible();
|
||||
expect(screen.queryByText(TEXTS.PLUGIN_TITLE)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders an error message if there are any errors while importing the plugin', async () => {
|
||||
const errorMsg = 'Unable to find plugin';
|
||||
useImportAppPluginMock.mockReturnValue({ value: undefined, loading: false, error: new Error(errorMsg) });
|
||||
|
||||
renderAppPlugin();
|
||||
|
||||
expect(await screen.findByText(errorMsg)).toBeVisible();
|
||||
expect(screen.queryByText(TEXTS.PLUGIN_TITLE)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function renderAppPlugin() {
|
||||
render(
|
||||
<Router history={locationService.getHistory()}>
|
||||
<AppPluginLoader id="foo" />;
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
class AppPluginComponent extends Component<AppRootProps> {
|
||||
static timesMounted = 0;
|
||||
|
||||
componentDidMount() {
|
||||
AppPluginComponent.timesMounted += 1;
|
||||
|
||||
const node: NavModelItem = {
|
||||
text: TEXTS.PLUGIN_TITLE,
|
||||
children: [
|
||||
{
|
||||
text: TEXTS.PLUGIN_TAB_TITLE_A,
|
||||
url: '/tab-a',
|
||||
id: 'a',
|
||||
},
|
||||
{
|
||||
text: TEXTS.PLUGIN_TAB_TITLE_B,
|
||||
url: '/tab-b',
|
||||
id: 'b',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
this.props.onNavChanged({
|
||||
main: node,
|
||||
node,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <p>{TEXTS.PLUGIN_CONTENT}</p>;
|
||||
}
|
||||
}
|
||||
|
||||
function getAppPluginMeta() {
|
||||
return getMockPlugin({
|
||||
type: PluginType.app,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
function getAppPluginMock() {
|
||||
const plugin = new AppPlugin();
|
||||
|
||||
plugin.root = AppPluginComponent;
|
||||
plugin.init(getAppPluginMeta());
|
||||
|
||||
return plugin;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { getWarningNav } from 'app/angular/services/nav_model_srv';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
|
||||
import { useImportAppPlugin } from '../hooks/useImportAppPlugin';
|
||||
|
||||
type AppPluginLoaderProps = {
|
||||
// The id of the app plugin to be loaded
|
||||
id: string;
|
||||
// The base URL path - defaults to the current path
|
||||
basePath?: string;
|
||||
};
|
||||
|
||||
// This component can be used to render an app-plugin based on its plugin ID.
|
||||
export const AppPluginLoader = ({ id, basePath }: AppPluginLoaderProps) => {
|
||||
const [nav, setNav] = useState<NavModel | null>(null);
|
||||
const { value: plugin, error, loading } = useImportAppPlugin(id);
|
||||
const queryParams = useParams();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
if (error) {
|
||||
return <Page.Header model={getWarningNav(error.message, error.stack)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <PageLoader />}
|
||||
{nav && <Page.Header model={nav} />}
|
||||
{!loading && plugin && plugin.root && (
|
||||
<plugin.root
|
||||
meta={plugin.meta}
|
||||
basename={basePath || pathname}
|
||||
onNavChanged={setNav}
|
||||
query={queryParams}
|
||||
path={pathname}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import { render, act, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AppPlugin, PluginType } from '@grafana/data';
|
||||
|
||||
import { getMockPlugin } from '../../__mocks__/pluginMocks';
|
||||
import { getPluginSettings } from '../../pluginSettings';
|
||||
import { importAppPlugin } from '../../plugin_loader';
|
||||
import { useImportAppPlugin } from '../useImportAppPlugin';
|
||||
|
||||
jest.mock('../../pluginSettings', () => ({
|
||||
getPluginSettings: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../plugin_loader', () => ({
|
||||
importAppPlugin: jest.fn(),
|
||||
}));
|
||||
|
||||
const importAppPluginMock = importAppPlugin as jest.Mock<
|
||||
ReturnType<typeof importAppPlugin>,
|
||||
Parameters<typeof importAppPlugin>
|
||||
>;
|
||||
|
||||
const getPluginSettingsMock = getPluginSettings as jest.Mock<
|
||||
ReturnType<typeof getPluginSettings>,
|
||||
Parameters<typeof getPluginSettings>
|
||||
>;
|
||||
|
||||
const PLUGIN_ID = 'sample-plugin';
|
||||
|
||||
describe('useImportAppPlugin()', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('returns the imported plugin in case it exists', async () => {
|
||||
let response: any;
|
||||
getPluginSettingsMock.mockResolvedValue(getAppPluginMeta());
|
||||
importAppPluginMock.mockResolvedValue(getAppPluginMock());
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error).toBeUndefined());
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns an error if the plugin does not exist', async () => {
|
||||
let response: any;
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toMatch(/unknown plugin/i));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns an error if the plugin is not an app', async () => {
|
||||
let response: any;
|
||||
getPluginSettingsMock.mockResolvedValue(getAppPluginMeta({ type: PluginType.panel }));
|
||||
importAppPluginMock.mockResolvedValue(getAppPluginMock());
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toMatch(/plugin must be an app/i));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns an error if the plugin is not enabled', async () => {
|
||||
let response: any;
|
||||
getPluginSettingsMock.mockResolvedValue(getAppPluginMeta({ enabled: false }));
|
||||
importAppPluginMock.mockResolvedValue(getAppPluginMock());
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toMatch(/is not enabled/i));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns errors that happen during fetching plugin settings', async () => {
|
||||
let response: any;
|
||||
const errorMsg = 'Error while fetching plugin data';
|
||||
getPluginSettingsMock.mockRejectedValue(new Error(errorMsg));
|
||||
importAppPluginMock.mockResolvedValue(getAppPluginMock());
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toBe(errorMsg));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
|
||||
test('returns errors that happen during importing a plugin', async () => {
|
||||
let response: any;
|
||||
const errorMsg = 'Error while importing the plugin';
|
||||
getPluginSettingsMock.mockResolvedValue(getAppPluginMeta());
|
||||
importAppPluginMock.mockRejectedValue(new Error(errorMsg));
|
||||
|
||||
act(() => {
|
||||
response = runHook(PLUGIN_ID);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(response.value).toBeUndefined());
|
||||
await waitFor(() => expect(response.error).not.toBeUndefined());
|
||||
await waitFor(() => expect(response.error.message).toBe(errorMsg));
|
||||
await waitFor(() => expect(response.loading).toBe(false));
|
||||
});
|
||||
});
|
||||
|
||||
function runHook(id: string): any {
|
||||
const returnVal = {};
|
||||
function TestComponent() {
|
||||
Object.assign(returnVal, useImportAppPlugin(id));
|
||||
return null;
|
||||
}
|
||||
render(<TestComponent />);
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
function getAppPluginMeta(overrides?: Record<string, any>) {
|
||||
return getMockPlugin({
|
||||
id: PLUGIN_ID,
|
||||
type: PluginType.app,
|
||||
enabled: true,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function getAppPluginMock() {
|
||||
const plugin = new AppPlugin();
|
||||
|
||||
plugin.init(getAppPluginMeta());
|
||||
|
||||
return plugin;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
import { PluginType } from '@grafana/data';
|
||||
|
||||
import { getPluginSettings } from '../pluginSettings';
|
||||
import { importAppPlugin } from '../plugin_loader';
|
||||
|
||||
export const useImportAppPlugin = (id: string) => {
|
||||
return useAsync(async () => {
|
||||
const pluginMeta = await getPluginSettings(id);
|
||||
|
||||
if (!pluginMeta) {
|
||||
throw new Error(`Unknown plugin: "${id}"`);
|
||||
}
|
||||
|
||||
if (pluginMeta.type !== PluginType.app) {
|
||||
throw new Error(`Plugin must be an app (currently "${pluginMeta.type}")`);
|
||||
}
|
||||
|
||||
if (!pluginMeta.enabled) {
|
||||
throw new Error(`Application "${id}" is not enabled`);
|
||||
}
|
||||
|
||||
return await importAppPlugin(pluginMeta);
|
||||
});
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
transformDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend';
|
||||
import { StreamingDataFrame } from 'app/features/live/data/StreamingDataFrame';
|
||||
import { isStreamingDataFrame } from 'app/features/live/data/utils';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
@@ -235,12 +236,13 @@ export class PanelQueryRunner {
|
||||
|
||||
try {
|
||||
const ds = await getDataSource(datasource, request.scopedVars);
|
||||
|
||||
const isMixedDS = ds.meta?.mixed;
|
||||
// Attach the data source to each query
|
||||
request.targets = request.targets.map((query) => {
|
||||
const isExpressionQuery = query.datasource?.type === ExpressionDatasourceRef.type;
|
||||
// When using a data source variable, the panel might have the incorrect datasource
|
||||
// stored, so when running the query make sure it is done with the correct one
|
||||
if (!query.datasource || (query.datasource.uid !== ds.uid && !ds.meta.mixed)) {
|
||||
if (!query.datasource || (query.datasource.uid !== ds.uid && !isMixedDS && !isExpressionQuery)) {
|
||||
query.datasource = ds.getRef();
|
||||
}
|
||||
return query;
|
||||
|
||||
@@ -144,10 +144,12 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
z-index: ${theme.zIndex.sidemenu};
|
||||
position: fixed;
|
||||
background: ${theme.colors.background.canvas};
|
||||
padding: ${theme.spacing(1)};
|
||||
|
||||
${theme.breakpoints.up('md')} {
|
||||
left: ${theme.components.sidemenu.width}px;
|
||||
z-index: ${theme.zIndex.navbarFixed + 1};
|
||||
padding: ${theme.spacing(2)};
|
||||
}
|
||||
`,
|
||||
container: css`
|
||||
@@ -155,11 +157,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
flex-direction: column;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: ${theme.spacing(2)};
|
||||
padding: ${theme.spacing(1)};
|
||||
background: ${theme.colors.background.primary};
|
||||
border: 1px solid ${theme.components.panel.borderColor};
|
||||
margin-top: ${theme.spacing(4)};
|
||||
|
||||
height: 100%;
|
||||
|
||||
${theme.breakpoints.up('md')} {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { Props, TeamSettings } from './TeamSettings';
|
||||
import { getMockTeam } from './__mocks__/teamMocks';
|
||||
|
||||
jest.mock('app/core/core', () => ({
|
||||
jest.mock('app/core/services/context_srv', () => ({
|
||||
contextSrv: {
|
||||
hasPermissionInMetadata: () => true,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('app/core/components/SharedPreferences/SharedPreferences', () => {
|
||||
return { SharedPreferences: () => <div /> };
|
||||
});
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
team: getMockTeam(),
|
||||
@@ -18,19 +23,36 @@ const setup = (propOverrides?: object) => {
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<TeamSettings {...props} />);
|
||||
const instance = wrapper.instance() as any;
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance,
|
||||
};
|
||||
render(<TeamSettings {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
describe('Team settings', () => {
|
||||
it('should render component', () => {
|
||||
const { wrapper } = setup();
|
||||
setup();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(screen.getByText('Team settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
const mockUpdate = jest.fn();
|
||||
setup({ updateTeam: mockUpdate });
|
||||
await userEvent.clear(screen.getByRole('textbox', { name: /Name/ }));
|
||||
await userEvent.type(screen.getByLabelText(/Email/i), 'team@test.com');
|
||||
// Submitting with userEvent doesn't work here
|
||||
fireEvent.submit(screen.getByRole('button', { name: 'Update' }));
|
||||
expect(await screen.findByText('Name is required')).toBeInTheDocument();
|
||||
await waitFor(() => expect(mockUpdate).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should submit form with correct values', async () => {
|
||||
const mockUpdate = jest.fn();
|
||||
setup({ updateTeam: mockUpdate });
|
||||
await userEvent.clear(screen.getByRole('textbox', { name: /Name/ }));
|
||||
await userEvent.clear(screen.getByLabelText(/Email/i));
|
||||
await userEvent.type(screen.getByRole('textbox', { name: /Name/ }), 'New team');
|
||||
await userEvent.type(screen.getByLabelText(/Email/i), 'team@test.com');
|
||||
// Submitting with userEvent doesn't work here
|
||||
fireEvent.submit(screen.getByRole('button', { name: 'Update' }));
|
||||
await waitFor(() => expect(mockUpdate).toHaveBeenCalledWith('New team', 'team@test.com'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,9 +32,15 @@ export const TeamSettings: FC<Props> = ({ team, updateTeam }) => {
|
||||
}}
|
||||
disabled={!canWriteTeamSettings}
|
||||
>
|
||||
{({ register }) => (
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<Field label="Name" disabled={!canWriteTeamSettings}>
|
||||
<Field
|
||||
label="Name"
|
||||
disabled={!canWriteTeamSettings}
|
||||
required
|
||||
invalid={!!errors.name}
|
||||
error="Name is required"
|
||||
>
|
||||
<Input {...register('name', { required: true })} id="name-input" />
|
||||
</Field>
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<VerticalGroup>
|
||||
<FieldSet
|
||||
label="Team settings"
|
||||
>
|
||||
<Form
|
||||
defaultValues={
|
||||
Object {
|
||||
"avatarUrl": "some/url/",
|
||||
"email": "test@test.com",
|
||||
"id": 1,
|
||||
"memberCount": 1,
|
||||
"name": "test",
|
||||
"permission": 0,
|
||||
}
|
||||
}
|
||||
disabled={true}
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<Component />
|
||||
</Form>
|
||||
</FieldSet>
|
||||
<SharedPreferences
|
||||
disabled={true}
|
||||
resourceUri="teams/1"
|
||||
/>
|
||||
</VerticalGroup>
|
||||
`;
|
||||
+23
-9
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
|
||||
import { QueryEditorProps, toOption } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Input } from '@grafana/ui';
|
||||
|
||||
import { INPUT_WIDTH } from '../constants';
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
AlignmentTypes,
|
||||
} from '../types';
|
||||
|
||||
import { MetricQueryEditor as ExperimentalMetricQueryEditor } from './Experimental/MetricQueryEditor';
|
||||
import { MetricQueryEditor } from './MetricQueryEditor';
|
||||
|
||||
import { AnnotationsHelp, QueryEditorRow } from './';
|
||||
@@ -78,15 +80,27 @@ export const AnnotationQueryEditor = (props: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={handleQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={metricQuery}
|
||||
/>
|
||||
{config.featureToggles.cloudMonitoringExperimentalUI ? (
|
||||
<ExperimentalMetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={handleQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={metricQuery}
|
||||
/>
|
||||
) : (
|
||||
<MetricQueryEditor
|
||||
refId={query.refId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
customMetaData={customMetaData}
|
||||
onChange={handleQueryChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource}
|
||||
query={metricQuery}
|
||||
/>
|
||||
)}
|
||||
|
||||
<QueryEditorRow label="Title" htmlFor="annotation-query-title">
|
||||
<Input id="annotation-query-title" value={title} width={INPUT_WIDTH} onChange={handleTitleChange} />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { debounce } from 'lodash';
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
|
||||
import { EditorRow, EditorField } from '@grafana/experimental';
|
||||
import { Input } from '@grafana/ui';
|
||||
|
||||
import { SELECT_WIDTH } from '../../constants';
|
||||
|
||||
export interface Props {
|
||||
refId: string;
|
||||
onChange: (alias: any) => void;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export const AliasBy: FunctionComponent<Props> = ({ refId, value = '', onChange }) => {
|
||||
const [alias, setAlias] = useState(value ?? '');
|
||||
|
||||
const propagateOnChange = debounce(onChange, 1000);
|
||||
|
||||
onChange = (e: any) => {
|
||||
setAlias(e.target.value);
|
||||
propagateOnChange(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorRow>
|
||||
<EditorField label="Alias by">
|
||||
<Input id={`${refId}-alias-by`} width={SELECT_WIDTH} value={alias} onChange={onChange} />
|
||||
</EditorField>
|
||||
</EditorRow>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user