Compare commits
15 Commits
ihm/251217
...
docs/gcm-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06297cb703 | ||
|
|
ec941b42ef | ||
|
|
873d35b494 | ||
|
|
d191425f3d | ||
|
|
eaaaffd14a | ||
|
|
03be2833dc | ||
|
|
c34766014c | ||
|
|
0a66aacfb3 | ||
|
|
0382bec6c7 | ||
|
|
32d6d7fe40 | ||
|
|
fb975fc21a | ||
|
|
cd37c3b827 | ||
|
|
246f615197 | ||
|
|
4a886591a5 | ||
|
|
a58ac26ef4 |
@@ -43,169 +43,126 @@ refs:
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/provisioning/#data-sources
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/provisioning/#data-sources
|
||||
alerting:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/
|
||||
variables:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/variables/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/visualizations/dashboards/variables/
|
||||
annotate-visualizations:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/visualizations/dashboards/build-dashboards/annotate-visualizations/
|
||||
transformations:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/query-transform-data/transform-data/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/visualizations/panels-visualizations/query-transform-data/transform-data/
|
||||
visualizations:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/
|
||||
configure-gcm:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/configure/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/configure/
|
||||
query-editor:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/query-editor/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/query-editor/
|
||||
template-variables:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/template-variables/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/template-variables/
|
||||
annotations-gcm:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/annotations/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/annotations/
|
||||
alerting-gcm:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/alerting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/alerting/
|
||||
google-authentication:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/
|
||||
troubleshooting:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/troubleshooting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/troubleshooting/
|
||||
---
|
||||
|
||||
# Google Cloud Monitoring data source
|
||||
|
||||
Grafana ships with built-in support for Google Cloud Monitoring.
|
||||
This topic describes queries, templates, variables, and other configuration specific to the Google Cloud Monitoring data source.
|
||||
Google Cloud Monitoring (formerly Stackdriver) is Google Cloud Platform's native monitoring and observability service that collects metrics, events, and metadata from GCP resources, hosted uptime probes, and application instrumentation. The Google Cloud Monitoring data source in Grafana allows you to query and visualize this data alongside metrics from other systems, creating unified dashboards for comprehensive infrastructure and application monitoring.
|
||||
|
||||
For instructions on how to add a data source to Grafana, refer to the [administration documentation](ref:data-source-management).
|
||||
Only users with the organization administrator role can add data sources.
|
||||
Grafana includes native support for the Google Cloud Monitoring plugin, so you don't need to install a plugin.
|
||||
|
||||
Once you've added the Google Cloud Monitoring data source, you can [configure it](#configure-the-data-source) so that your Grafana instance's users can create queries in its [query editor](query-editor/) and apply [annotations](#annotations) when they [build dashboards](ref:build-dashboards) and use [Explore](ref:explore).
|
||||
## Get started
|
||||
|
||||
## Before you begin
|
||||
The following documents will help you get started with the Google Cloud Monitoring data source:
|
||||
|
||||
Complete the following before you configure the data source.
|
||||
- [Configure the data source](ref:configure-gcm) - Set up authentication and connect to Google Cloud
|
||||
- [Query editor](ref:query-editor) - Create and edit Metric and SLO queries
|
||||
- [Template variables](ref:template-variables) - Create dynamic dashboards with Google Cloud Monitoring variables
|
||||
- [Annotations](ref:annotations-gcm) - Overlay Google Cloud Monitoring events on graphs
|
||||
- [Alerting](ref:alerting-gcm) - Create alert rules based on GCP metrics and SLOs
|
||||
- [Google authentication](ref:google-authentication) - Configure authentication methods for GCP access
|
||||
- [Troubleshooting](ref:troubleshooting) - Solve common configuration and query errors
|
||||
|
||||
### Configure Google authentication
|
||||
## Supported query types
|
||||
|
||||
Before you can request data from Google Cloud Monitoring, you must configure authentication.
|
||||
All requests to Google APIs are performed on the server-side by the Grafana backend.
|
||||
The Google Cloud Monitoring data source supports the following query types:
|
||||
|
||||
For authentication options and configuration details, refer to [Google authentication](google-authentication/).
|
||||
| Query type | Description |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| **Metrics** | Query time series data from GCP resources using the Monitoring Query Language (MQL) or the visual builder. |
|
||||
| **Service Level Objectives (SLOs)** | Query SLO data defined in Google Cloud Monitoring to track service reliability and error budgets. |
|
||||
|
||||
When configuring Google authentication, keep in mind the following additional steps related to Google Cloud Monitoring.
|
||||
## Additional features
|
||||
|
||||
#### Configure a GCP Service Account
|
||||
After you configure the Google Cloud Monitoring data source, you can:
|
||||
|
||||
When you [create a Google Cloud Platform (GCP) Service Account and key file](google-authentication/#create-a-gcp-service-account-and-key-file), the Service Account must have the **Monitoring Viewer** role (**Role > Select a role > Monitoring > Monitoring Viewer**):
|
||||
- Create a wide variety of [visualizations](ref:visualizations) using GCP metrics.
|
||||
- Configure and use [template variables](ref:variables) for dynamic dashboards.
|
||||
- Add [transformations](ref:transformations) to manipulate query results.
|
||||
- Add [annotations](ref:annotate-visualizations) to overlay events on your graphs.
|
||||
- Set up [alerting](ref:alerting) based on GCP metrics.
|
||||
- Use [Explore](ref:explore) to investigate your Google Cloud data without building a dashboard.
|
||||
|
||||
{{< figure src="/static/img/docs/v71/cloudmonitoring_service_account_choose_role.png" max-width="600px" class="docs-image--no-shadow" caption="Choose role" >}}
|
||||
## Pre-configured dashboards
|
||||
|
||||
#### Grant the GCE Default Service Account scope
|
||||
|
||||
If Grafana is running on a Google Compute Engine (GCE) virtual machine, when you [Configure a GCE Default Service Account](google-authentication/#configure-a-gce-default-service-account), you must also grant that Service Account access to the "Cloud Monitoring API" scope.
|
||||
|
||||
### Enable necessary Google Cloud Platform APIs
|
||||
|
||||
Before you can request data from Google Cloud Monitoring, you must first enable necessary APIs on the Google end.
|
||||
|
||||
1. Open the Monitoring and Cloud Resource Manager API pages:
|
||||
- [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com)
|
||||
- [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com)
|
||||
|
||||
1. On each page, click the `Enable` button.
|
||||
|
||||
{{< figure src="/static/img/docs/v71/cloudmonitoring_enable_api.png" max-width="450px" class="docs-image--no-shadow" caption="Enable GCP APIs" >}}
|
||||
|
||||
## Configure the data source
|
||||
|
||||
To configure basic settings for the data source, complete the following steps:
|
||||
|
||||
1. Click **Connections** in the left-side menu.
|
||||
1. Under Your connections, click **Data sources**.
|
||||
1. Enter `Google Cloud Monitoring` in the search bar.
|
||||
1. Click **Google Cloud Monitoring**.
|
||||
|
||||
The **Settings** tab of the data source is displayed.
|
||||
|
||||
1. Set the data source's basic configuration options:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| **Universe Domain** | The universe domain to connect to. For more information, refer to [Documentation on universe domains](https://docs.cloud.google.com/python/docs/reference/monitoring/latest/google.cloud.monitoring_v3.services.service_monitoring_service.ServiceMonitoringServiceAsyncClient#google_cloud_monitoring_v3_services_service_monitoring_service_ServiceMonitoringServiceAsyncClient_universe_domain). Defaults to `googleapis.com`. |
|
||||
|
||||
### Provision the data source
|
||||
|
||||
You can define and configure the data source in YAML files as part of the Grafana provisioning system.
|
||||
For more information about provisioning, and for available configuration options, refer to [Provisioning Grafana](ref:provisioning-data-sources).
|
||||
|
||||
#### Provisioning examples
|
||||
|
||||
**Using the JWT (Service Account key file) authentication type:**
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Google Cloud Monitoring
|
||||
type: stackdriver
|
||||
access: proxy
|
||||
jsonData:
|
||||
tokenUri: https://oauth2.googleapis.com/token
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
secureJsonData:
|
||||
privateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
POSEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb1u1Srw8ICYHS
|
||||
...
|
||||
yA+23427282348234=
|
||||
-----END PRIVATE KEY-----
|
||||
```
|
||||
|
||||
**Using the JWT (Service Account private key path) authentication type:**
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Google Cloud Monitoring
|
||||
type: stackdriver
|
||||
access: proxy
|
||||
jsonData:
|
||||
tokenUri: https://oauth2.googleapis.com/token
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
privateKeyPath: /etc/secrets/gce.pem
|
||||
```
|
||||
|
||||
**Using GCE Default Service Account authentication:**
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Google Cloud Monitoring
|
||||
type: stackdriver
|
||||
access: proxy
|
||||
jsonData:
|
||||
authenticationType: gce
|
||||
universeDomain: googleapis.com
|
||||
```
|
||||
|
||||
## Import pre-configured dashboards
|
||||
|
||||
The Google Cloud Monitoring data source ships with pre-configured dashboards for some of the most popular GCP services.
|
||||
These curated dashboards are based on similar dashboards in the GCP dashboard samples repository.
|
||||
The Google Cloud Monitoring data source includes pre-configured dashboards for popular GCP services. These curated dashboards are based on similar dashboards in the GCP dashboard samples repository.
|
||||
|
||||
{{< figure src="/static/img/docs/google-cloud-monitoring/curated-dashboards-7-4.png" class="docs-image--no-shadow" max-width="650px" caption="Curated dashboards for Google Cloud Monitoring" >}}
|
||||
|
||||
**To import curated dashboards:**
|
||||
To import a pre-configured dashboard:
|
||||
|
||||
1. Navigate to the data source's [configuration page](#configure-the-data-source).
|
||||
1. Select the **Dashboards** tab.
|
||||
1. Go to **Connections** > **Data sources**.
|
||||
1. Select your Google Cloud Monitoring data source.
|
||||
1. Click the **Dashboards** tab.
|
||||
1. Click **Import** next to the dashboard you want to use.
|
||||
|
||||
This displays the curated selection of importable dashboards.
|
||||
The dashboards include a [template variable](ref:template-variables) populated with the projects accessible by the configured [service account](ref:google-authentication) each time you load the dashboard. After Grafana loads the dashboard, you can select a project from the dropdown list.
|
||||
|
||||
1. Select **Import** for the dashboard to import.
|
||||
To customize an imported dashboard, save it under a different name. Otherwise, Grafana upgrades can overwrite your customizations with the new version.
|
||||
|
||||
The dashboards include a [template variable](template-variables/) populated with the projects accessible by the configured [Service Account](google-authentication/) each time you load the dashboard.
|
||||
After Grafana loads the dashboard, you can select a project from the dropdown list.
|
||||
## Related resources
|
||||
|
||||
**To customize an imported dashboard:**
|
||||
|
||||
To customize one of these dashboards, we recommend that you save it under a different name.
|
||||
If you don't, upgrading Grafana can overwrite the customized dashboard with the new version.
|
||||
|
||||
## Query the data source
|
||||
|
||||
The Google Cloud Monitoring query editor helps you build two types of queries: **Metric** and **Service Level Objective (SLO)**.
|
||||
|
||||
For details, refer to the [query editor documentation](query-editor/).
|
||||
|
||||
## Use template variables
|
||||
|
||||
Instead of hard-coding details such as server, application, and sensor names in metric queries, you can use variables.
|
||||
Grafana lists these variables in dropdown select boxes at the top of the dashboard to help you change the data displayed in your dashboard.
|
||||
Grafana refers to such variables as template variables.
|
||||
|
||||
For details, see the [template variables documentation](template-variables/).
|
||||
- [Google Cloud Monitoring documentation](https://cloud.google.com/monitoring/docs)
|
||||
- [Monitoring Query Language (MQL) reference](https://cloud.google.com/monitoring/mql/reference)
|
||||
- [Google Cloud metrics list](https://cloud.google.com/monitoring/api/metrics_gcp)
|
||||
- [Grafana community forum](https://community.grafana.com/)
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
---
|
||||
aliases:
|
||||
- ../../data-sources/google-cloud-monitoring/alerting/
|
||||
description: Set up alerts using Google Cloud Monitoring data in Grafana
|
||||
keywords:
|
||||
- grafana
|
||||
- google
|
||||
- cloud
|
||||
- monitoring
|
||||
- alerting
|
||||
- alerts
|
||||
- metrics
|
||||
- slo
|
||||
- recording rules
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Alerting
|
||||
title: Google Cloud Monitoring alerting
|
||||
weight: 450
|
||||
refs:
|
||||
alerting:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/
|
||||
alerting-fundamentals:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/
|
||||
create-alert-rule:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-grafana-managed-rule/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-grafana-managed-rule/
|
||||
configure-gcm:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/configure/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/configure/
|
||||
query-editor:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/query-editor/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/query-editor/
|
||||
troubleshoot:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/troubleshooting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/troubleshooting/
|
||||
recording-rules:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-recording-rules/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/create-recording-rules/
|
||||
---
|
||||
|
||||
# Google Cloud Monitoring alerting
|
||||
|
||||
The Google Cloud Monitoring data source supports [Grafana Alerting](ref:alerting), allowing you to create alert rules based on GCP metrics and Service Level Objectives (SLOs). You can monitor your Google Cloud environment and receive notifications when specific conditions are met.
|
||||
|
||||
## Before you begin
|
||||
|
||||
Before you create alert rules, ensure the following:
|
||||
|
||||
- You have appropriate permissions to create alert rules in Grafana.
|
||||
- Your Google Cloud Monitoring data source is configured and working correctly. Refer to [Configure the data source](ref:configure-gcm).
|
||||
- You're familiar with [Grafana Alerting concepts](ref:alerting-fundamentals).
|
||||
|
||||
## Supported query types for alerting
|
||||
|
||||
The following query types support alerting:
|
||||
|
||||
| Query type | Use case | Notes |
|
||||
| ---------------------------------- | ------------------------------------------------------ | -------------------------------------------------- |
|
||||
| **Builder** | Threshold-based alerts on GCP resource metrics | Best suited for alerting; returns time-series data |
|
||||
| **MQL** | Complex metric queries using Monitoring Query Language | Use for advanced filtering and aggregations |
|
||||
| **Service Level Objectives (SLO)** | Alert on SLO compliance, error budgets, or burn rate | Monitor service reliability |
|
||||
| **PromQL** | Prometheus-style queries on GCP metrics | Familiar syntax for Prometheus users |
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Alert queries must return numeric data that Grafana can evaluate against a threshold. Queries that return only text or non-numeric data can't be used directly for alerting.
|
||||
{{< /admonition >}}
|
||||
|
||||
## Authentication requirements
|
||||
|
||||
Alerting rules run as background processes without a user context. Both supported authentication methods work with alerting:
|
||||
|
||||
| Authentication method | Supported |
|
||||
| --------------------------- | --------- |
|
||||
| Google JWT File | ✓ |
|
||||
| GCE Default Service Account | ✓ |
|
||||
|
||||
## Create an alert rule
|
||||
|
||||
To create an alert rule using Google Cloud Monitoring data:
|
||||
|
||||
1. Go to **Alerting** > **Alert rules**.
|
||||
1. Click **New alert rule**.
|
||||
1. Enter a name for your alert rule.
|
||||
1. In the **Define query and alert condition** section:
|
||||
- Select your Google Cloud Monitoring data source.
|
||||
- Configure your query (for example, a Builder query for CPU usage or an SLO query for error budget).
|
||||
- Add a **Reduce** expression if your query returns multiple series.
|
||||
- Add a **Threshold** expression to define the alert condition.
|
||||
1. Configure the **Set evaluation behavior**:
|
||||
- Select or create a folder and evaluation group.
|
||||
- Set the evaluation interval (how often the alert is checked).
|
||||
- Set the pending period (how long the condition must be true before firing).
|
||||
1. Add labels and annotations to provide context for notifications.
|
||||
1. Click **Save rule**.
|
||||
|
||||
For detailed instructions, refer to [Create a Grafana-managed alert rule](ref:create-alert-rule).
|
||||
|
||||
## Example: VM CPU usage alert
|
||||
|
||||
This example creates an alert that fires when Compute Engine VM CPU utilization exceeds 80%:
|
||||
|
||||
1. Create a new alert rule.
|
||||
1. Configure the query:
|
||||
- **Query type**: Builder
|
||||
- **Project**: Select your GCP project
|
||||
- **Service**: Compute Engine
|
||||
- **Metric**: `instance/cpu/utilization`
|
||||
- **Group by function**: mean
|
||||
1. Add expressions:
|
||||
- **Reduce**: Last (to get the most recent data point)
|
||||
- **Threshold**: Is above 0.8 (CPU utilization is returned as a decimal)
|
||||
1. Set evaluation to run every 1 minute with a 5-minute pending period.
|
||||
1. Save the rule.
|
||||
|
||||
## Example: SLO error budget alert
|
||||
|
||||
This example alerts when an SLO's error budget remaining drops below 20%:
|
||||
|
||||
1. Create a new alert rule.
|
||||
1. Configure the query:
|
||||
- **Query type**: Service Level Objectives (SLO)
|
||||
- **Project**: Select your GCP project
|
||||
- **Service**: Select your SLO service
|
||||
- **SLO**: Select your SLO
|
||||
- **Selector**: SLO Error Budget Remaining
|
||||
1. Add expressions:
|
||||
- **Reduce**: Last
|
||||
- **Threshold**: Is below 0.2 (20% remaining)
|
||||
1. Set evaluation to run every 5 minutes.
|
||||
1. Save the rule.
|
||||
|
||||
## Example: Cloud SQL memory alert
|
||||
|
||||
This example alerts when Cloud SQL instance memory usage exceeds 90%:
|
||||
|
||||
1. Create a new alert rule.
|
||||
1. Configure the query:
|
||||
- **Query type**: Builder
|
||||
- **Project**: Select your GCP project
|
||||
- **Service**: Cloud SQL
|
||||
- **Metric**: `database/memory/utilization`
|
||||
- **Filter**: Add a filter for specific database instances if needed
|
||||
1. Add expressions:
|
||||
- **Reduce**: Last
|
||||
- **Threshold**: Is above 0.9
|
||||
1. Set evaluation to run every 1 minute.
|
||||
1. Save the rule.
|
||||
|
||||
## Best practices
|
||||
|
||||
Follow these recommendations to create reliable and efficient alerts with Google Cloud Monitoring data.
|
||||
|
||||
### Use appropriate query intervals
|
||||
|
||||
- Set the alert evaluation interval to be greater than or equal to the minimum data resolution from Google Cloud Monitoring.
|
||||
- Most GCP metrics have 1-minute granularity at minimum.
|
||||
- Avoid very short intervals (less than 1 minute) as they may cause evaluation timeouts or miss data points.
|
||||
|
||||
### Reduce multiple series
|
||||
|
||||
When your query returns multiple time series (for example, CPU usage across multiple VMs), use the **Reduce** expression to aggregate them:
|
||||
|
||||
- **Last**: Use the most recent value
|
||||
- **Mean**: Average across all series
|
||||
- **Max/Min**: Use the highest or lowest value
|
||||
- **Sum**: Total across all series
|
||||
|
||||
### Use appropriate alignment periods
|
||||
|
||||
For alerting queries, ensure the alignment period provides enough data points:
|
||||
|
||||
- Use "cloud monitoring auto" or "grafana auto" for most cases.
|
||||
- For more precise control, set a fixed alignment period that matches your evaluation interval.
|
||||
|
||||
### Handle no data conditions
|
||||
|
||||
Configure what happens when no data is returned:
|
||||
|
||||
1. In the alert rule, find **Configure no data and error handling**.
|
||||
1. Choose an appropriate action:
|
||||
- **No Data**: Keep the alert in its current state
|
||||
- **Alerting**: Treat no data as an alert condition
|
||||
- **OK**: Treat no data as a healthy state
|
||||
|
||||
### Test queries before alerting
|
||||
|
||||
Always verify your query returns expected data before creating an alert:
|
||||
|
||||
1. Go to **Explore**.
|
||||
1. Select your Google Cloud Monitoring data source.
|
||||
1. Run the query you plan to use for alerting.
|
||||
1. Confirm the data format and values are correct.
|
||||
1. Verify the query returns numeric data suitable for threshold evaluation.
|
||||
|
||||
## Recording rules
|
||||
|
||||
The Google Cloud Monitoring data source supports [Grafana-managed recording rules](ref:recording-rules). Recording rules periodically pre-compute frequently used or computationally expensive queries, saving the results as a new time series metric.
|
||||
|
||||
Use recording rules to:
|
||||
|
||||
- Reduce query load on Google Cloud Monitoring by pre-computing complex aggregations.
|
||||
- Create derived metrics from GCP data for use in alerts and dashboards.
|
||||
- Import Google Cloud Monitoring data into a Prometheus-compatible database.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Grafana-managed recording rules write results to a Prometheus-compatible database (such as Grafana Mimir or the Grafana Cloud managed Prometheus). You must configure a target data source for storing the recorded metrics.
|
||||
{{< /admonition >}}
|
||||
|
||||
For instructions on creating recording rules, refer to [Create recording rules](ref:recording-rules).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If your Google Cloud Monitoring alerts aren't working as expected, use the following sections to diagnose and resolve common issues.
|
||||
|
||||
### Alerts not firing
|
||||
|
||||
- Check that the query returns numeric data in Explore.
|
||||
- Ensure the evaluation interval allows enough time for data to be available.
|
||||
- Verify the threshold is set correctly (remember that many GCP metrics return decimals, not percentages).
|
||||
- Review the alert rule's health and any error messages in the Alerting UI.
|
||||
|
||||
### Authentication errors in alert evaluation
|
||||
|
||||
If you see authentication errors when alerts evaluate:
|
||||
|
||||
- Verify the service account has the **Monitoring Viewer** role.
|
||||
- If using a JWT key file, ensure it hasn't been deleted or revoked.
|
||||
- Check that the required APIs (Monitoring API, Cloud Resource Manager API) are enabled.
|
||||
|
||||
### Query timeout errors
|
||||
|
||||
- Increase the alignment period to reduce the number of data points.
|
||||
- Reduce the time range in the query.
|
||||
- Simplify complex MQL queries.
|
||||
- Add filters to narrow the result set.
|
||||
|
||||
For additional troubleshooting help, refer to [Troubleshoot Google Cloud Monitoring](ref:troubleshoot).
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Grafana Alerting documentation](ref:alerting)
|
||||
- [Create alert rules](ref:create-alert-rule)
|
||||
- [Create recording rules](ref:recording-rules)
|
||||
- [Google Cloud Monitoring query editor](ref:query-editor)
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
aliases:
|
||||
- ../../data-sources/google-cloud-monitoring/annotations/
|
||||
description: Use annotations to overlay Google Cloud Monitoring events on Grafana graphs
|
||||
keywords:
|
||||
- grafana
|
||||
- google
|
||||
- cloud
|
||||
- monitoring
|
||||
- annotations
|
||||
- events
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Annotations
|
||||
title: Google Cloud Monitoring annotations
|
||||
weight: 400
|
||||
refs:
|
||||
annotate-visualizations:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/dashboards/build-dashboards/annotate-visualizations/
|
||||
---
|
||||
|
||||
# Google Cloud Monitoring annotations
|
||||
|
||||
[Annotations](ref:annotate-visualizations) overlay rich event information on top of graphs. You can use annotations to mark important events, deployments, or incidents on your dashboards.
|
||||
|
||||
## Before you begin
|
||||
|
||||
Before you configure annotations, ensure you have the following:
|
||||
|
||||
- A configured Google Cloud Monitoring data source.
|
||||
- A dashboard where you want to add annotations.
|
||||
|
||||
## Annotation limitations
|
||||
|
||||
Keep the following limitations in mind when using annotations:
|
||||
|
||||
- **Performance:** Rendering annotations is expensive. Limit the number of rows returned to maintain dashboard performance.
|
||||
- **Native events:** There's no support for displaying Google Cloud Monitoring's native annotations and events. However, annotations work well with [custom metrics](https://cloud.google.com/monitoring/custom-metrics/) in Google Cloud Monitoring.
|
||||
|
||||
## Add an annotation query
|
||||
|
||||
To add an annotation query to a dashboard:
|
||||
|
||||
1. Open the dashboard where you want to add annotations.
|
||||
1. Click **Dashboard settings** (gear icon).
|
||||
1. Select **Annotations** in the left menu.
|
||||
1. Click **Add annotation query**.
|
||||
1. Select your Google Cloud Monitoring data source.
|
||||
1. Configure the annotation query using the query editor.
|
||||
|
||||
## Configure the annotation query
|
||||
|
||||
With the query editor for annotations, you can select a metric and filters to define which data points create annotations.
|
||||
|
||||
The **Title** and **Text** fields support templating and can use data returned from the query.
|
||||
|
||||
For example, the Title field could have the following text:
|
||||
|
||||
`{{metric.type}} has value: {{metric.value}}`
|
||||
|
||||
Example result: `monitoring.googleapis.com/uptime_check/http_status has this value: 502`
|
||||
|
||||
## Annotation patterns
|
||||
|
||||
Use the following patterns in the **Title** and **Text** fields to display metric data in your annotations:
|
||||
|
||||
| Pattern format | Description | Example | Result |
|
||||
| ------------------------ | --------------------------------- | -------------------------------- | ------------------------------------------------- |
|
||||
| `{{metric.value}}` | Value of the metric/point. | `{{metric.value}}` | `555` |
|
||||
| `{{metric.type}}` | Returns the full Metric Type. | `{{metric.type}}` | `compute.googleapis.com/instance/cpu/utilization` |
|
||||
| `{{metric.name}}` | Returns the metric name part. | `{{metric.name}}` | `instance/cpu/utilization` |
|
||||
| `{{metric.service}}` | Returns the service part. | `{{metric.service}}` | `compute` |
|
||||
| `{{metric.label.xxx}}` | Returns the metric label value. | `{{metric.label.instance_name}}` | `grafana-1-prod` |
|
||||
| `{{resource.label.xxx}}` | Returns the resource label value. | `{{resource.label.zone}}` | `us-east1-b` |
|
||||
|
||||
## Example: Annotate uptime check failures
|
||||
|
||||
To create annotations for uptime check failures:
|
||||
|
||||
1. Add an annotation query using the Google Cloud Monitoring data source.
|
||||
1. Select the `monitoring.googleapis.com/uptime_check/check_passed` metric.
|
||||
1. Add a filter for `check_passed = false`.
|
||||
1. Set the **Title** to: `Uptime check failed: {{metric.label.check_id}}`
|
||||
1. Set the **Text** to: `Region: {{resource.label.zone}}`
|
||||
|
||||
This creates an annotation marker on your graph each time an uptime check fails.
|
||||
@@ -0,0 +1,363 @@
|
||||
---
|
||||
aliases:
|
||||
- ../../data-sources/google-cloud-monitoring/configure/
|
||||
description: This document provides configuration instructions for the Google Cloud Monitoring data source.
|
||||
keywords:
|
||||
- grafana
|
||||
- google
|
||||
- cloud
|
||||
- monitoring
|
||||
- stackdriver
|
||||
- configure
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Configure
|
||||
title: Configure the Google Cloud Monitoring data source
|
||||
weight: 100
|
||||
refs:
|
||||
provisioning-data-sources:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/provisioning/#data-sources
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/provisioning/#data-sources
|
||||
private-data-source-connect:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
|
||||
configure-pdc:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/configure-pdc/#configure-grafana-private-data-source-connect-pdc
|
||||
google-authentication:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/
|
||||
google-authentication-jwt:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/#create-a-gcp-service-account-and-key-file
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/#create-a-gcp-service-account-and-key-file
|
||||
google-authentication-gce:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/#use-gce-default-service-account
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/#use-gce-default-service-account
|
||||
query-editor:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/query-editor/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/query-editor/
|
||||
template-variables:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/template-variables/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/template-variables/
|
||||
annotations:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/annotations/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/annotations/
|
||||
alerting:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/alerting/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/alerting/
|
||||
explore:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/explore/
|
||||
gcm-index:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/
|
||||
---
|
||||
|
||||
# Configure the Google Cloud Monitoring data source
|
||||
|
||||
This document provides instructions for configuring the Google Cloud Monitoring data source in Grafana.
|
||||
|
||||
## Before you begin
|
||||
|
||||
Before you begin, ensure you have the following:
|
||||
|
||||
- **Grafana permissions:** You must have the `Organization administrator` role to configure data sources.
|
||||
- **GCP project:** A Google Cloud Platform project.
|
||||
- **GCP permissions:** Permissions to create a service account or configure GCE default service account settings in your GCP project.
|
||||
|
||||
Grafana includes a built-in Google Cloud Monitoring data source plugin, so you don't need to install a plugin.
|
||||
|
||||
## Set up GCP authentication
|
||||
|
||||
Before you can request data from Google Cloud Monitoring, you must configure authentication.
|
||||
All requests to Google APIs are performed on the server-side by the Grafana backend.
|
||||
|
||||
For authentication options and configuration details, refer to [Google authentication](ref:google-authentication).
|
||||
|
||||
When you configure Google authentication, note the following requirements specific to Google Cloud Monitoring.
|
||||
|
||||
### Configure a GCP Service Account
|
||||
|
||||
When you [create a Google Cloud Platform (GCP) Service Account and key file](ref:google-authentication-jwt), the Service Account must have the **Monitoring Viewer** role (**Role > Select a role > Monitoring > Monitoring Viewer**):
|
||||
|
||||
{{< figure src="/static/img/docs/v71/cloudmonitoring_service_account_choose_role.png" max-width="600px" class="docs-image--no-shadow" caption="Choose role" >}}
|
||||
|
||||
### Grant the GCE Default Service Account scope
|
||||
|
||||
If Grafana is running on a Google Compute Engine (GCE) virtual machine, when you [configure a GCE Default Service Account](ref:google-authentication-gce), you must also grant that Service Account access to the "Cloud Monitoring API" scope.
|
||||
|
||||
## Enable Google Cloud Platform APIs
|
||||
|
||||
Before you can request data from Google Cloud Monitoring, you must enable the necessary APIs in your GCP project.
|
||||
|
||||
1. Open the Monitoring and Cloud Resource Manager API pages:
|
||||
- [Monitoring API](https://console.cloud.google.com/apis/library/monitoring.googleapis.com)
|
||||
- [Cloud Resource Manager API](https://console.cloud.google.com/apis/library/cloudresourcemanager.googleapis.com)
|
||||
|
||||
1. On each page, click **Enable**.
|
||||
|
||||
{{< figure src="/static/img/docs/v71/cloudmonitoring_enable_api.png" max-width="450px" class="docs-image--no-shadow" caption="Enable GCP APIs" >}}
|
||||
|
||||
## Add the data source
|
||||
|
||||
To add the Google Cloud Monitoring data source:
|
||||
|
||||
1. Click **Connections** in the left-side menu.
|
||||
1. Click **Add new connection**.
|
||||
1. Enter `Google Cloud Monitoring` in the search bar.
|
||||
1. Select **Google Cloud Monitoring**.
|
||||
1. Click **Add new data source** in the upper right.
|
||||
|
||||
You're taken to the **Settings** tab where you configure the data source.
|
||||
|
||||
## Configure the data source in the UI
|
||||
|
||||
The following are configuration options for the Google Cloud Monitoring data source.
|
||||
|
||||
| Setting | Description |
|
||||
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
|
||||
| **Default** | Sets whether the data source is pre-selected for new panels. |
|
||||
| **Universe Domain** | The universe domain to connect to. For more information, refer to the [Google Cloud universe domains documentation](https://cloud.google.com/docs/overview#universe_domains). Defaults to `googleapis.com`. |
|
||||
|
||||
### Authentication
|
||||
|
||||
Configure how Grafana authenticates with Google Cloud.
|
||||
|
||||
| Setting | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Authentication type** | Select the authentication method. Choose **Google JWT File** to use a service account key file, or **GCE Default Service Account** if Grafana is running on a GCE virtual machine. |
|
||||
|
||||
### JWT Key Details
|
||||
|
||||
These settings appear when you select **Google JWT File** as the authentication type.
|
||||
|
||||
| Setting | Description |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **JWT token** | Upload or paste your Google JWT token. You can drag and drop a `.json` key file, click **Click to browse files** to upload, or use **Paste JWT Token** or **Fill In JWT Token manually**. |
|
||||
|
||||
### Service account impersonation
|
||||
|
||||
Use service account impersonation to have Grafana authenticate as a different service account than the one provided in the JWT token.
|
||||
|
||||
| Setting | Description |
|
||||
| ---------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **Enable** | Toggle to enable service account impersonation. |
|
||||
| **Service account to impersonate** | Enter the email address of the service account to impersonate when making requests to Google Cloud. |
|
||||
|
||||
### Private data source connect
|
||||
|
||||
_Only available for Grafana Cloud._
|
||||
|
||||
Use private data source connect (PDC) to connect to and query data within a secure network without opening that network to inbound traffic from Grafana Cloud. For more information on how PDC works, refer to [Private data source connect](ref:private-data-source-connect). For steps on setting up a PDC connection, refer to [Configure Grafana private data source connect (PDC)](ref:configure-pdc).
|
||||
|
||||
| Setting | Description |
|
||||
| ------------------------------- | --------------------------------------------------------------------------- |
|
||||
| **Private data source connect** | Select a PDC connection from the drop-down menu or create a new connection. |
|
||||
|
||||
### Save and test
|
||||
|
||||
Click **Save & test** to test the connection. A successful connection displays the following message:
|
||||
|
||||
`Successfully queried the Google Cloud Monitoring API.`
|
||||
|
||||
## Provision the data source
|
||||
|
||||
You can define and configure the data source in YAML files as part of the Grafana provisioning system.
|
||||
For more information about provisioning, and for available configuration options, refer to [Provisioning Grafana](ref:provisioning-data-sources).
|
||||
|
||||
### Provisioning examples
|
||||
|
||||
**Using the JWT (Service Account key file) authentication type:**
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Google Cloud Monitoring
|
||||
type: stackdriver
|
||||
access: proxy
|
||||
jsonData:
|
||||
tokenUri: https://oauth2.googleapis.com/token
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
secureJsonData:
|
||||
privateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
POSEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCb1u1Srw8ICYHS
|
||||
...
|
||||
yA+23427282348234=
|
||||
-----END PRIVATE KEY-----
|
||||
```
|
||||
|
||||
**Using the JWT (Service Account private key path) authentication type:**
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Google Cloud Monitoring
|
||||
type: stackdriver
|
||||
access: proxy
|
||||
jsonData:
|
||||
tokenUri: https://oauth2.googleapis.com/token
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
universeDomain: googleapis.com
|
||||
privateKeyPath: /etc/secrets/gce.pem
|
||||
```
|
||||
|
||||
**Using GCE Default Service Account authentication:**
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Google Cloud Monitoring
|
||||
type: stackdriver
|
||||
access: proxy
|
||||
jsonData:
|
||||
authenticationType: gce
|
||||
universeDomain: googleapis.com
|
||||
```
|
||||
|
||||
## Provision the data source using Terraform
|
||||
|
||||
You can provision the Google Cloud Monitoring data source using [Terraform](https://www.terraform.io/) with the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs).
|
||||
|
||||
For more information about provisioning resources with Terraform, refer to the [Grafana as code using Terraform](https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/terraform/) documentation.
|
||||
|
||||
### Terraform prerequisites
|
||||
|
||||
Before you begin, ensure you have the following:
|
||||
|
||||
- [Terraform](https://www.terraform.io/downloads) installed.
|
||||
- Grafana Terraform provider configured with appropriate credentials.
|
||||
- For Grafana Cloud: A [Cloud Access Policy token](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/) with data source permissions.
|
||||
|
||||
### Provider configuration
|
||||
|
||||
Configure the Grafana provider to connect to your Grafana instance:
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_providers {
|
||||
grafana = {
|
||||
source = "grafana/grafana"
|
||||
version = ">= 2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# For Grafana Cloud
|
||||
provider "grafana" {
|
||||
url = "<YOUR_GRAFANA_CLOUD_STACK_URL>"
|
||||
auth = "<YOUR_SERVICE_ACCOUNT_TOKEN>"
|
||||
}
|
||||
|
||||
# For self-hosted Grafana
|
||||
# provider "grafana" {
|
||||
# url = "http://localhost:3000"
|
||||
# auth = "<API_KEY_OR_SERVICE_ACCOUNT_TOKEN>"
|
||||
# }
|
||||
```
|
||||
|
||||
### Terraform examples
|
||||
|
||||
The following examples show how to configure the Google Cloud Monitoring data source for each authentication method.
|
||||
|
||||
**Using the JWT (Service Account key file) authentication type:**
|
||||
|
||||
```hcl
|
||||
resource "grafana_data_source" "google_cloud_monitoring" {
|
||||
type = "stackdriver"
|
||||
name = "Google Cloud Monitoring"
|
||||
|
||||
json_data_encoded = jsonencode({
|
||||
tokenUri = "https://oauth2.googleapis.com/token"
|
||||
clientEmail = "<SERVICE_ACCOUNT_EMAIL>"
|
||||
authenticationType = "jwt"
|
||||
defaultProject = "<GCP_PROJECT_ID>"
|
||||
universeDomain = "googleapis.com"
|
||||
})
|
||||
|
||||
secure_json_data_encoded = jsonencode({
|
||||
privateKey = "<PRIVATE_KEY_CONTENT>"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Using the JWT (Service Account private key path) authentication type:**
|
||||
|
||||
```hcl
|
||||
resource "grafana_data_source" "google_cloud_monitoring" {
|
||||
type = "stackdriver"
|
||||
name = "Google Cloud Monitoring"
|
||||
|
||||
json_data_encoded = jsonencode({
|
||||
tokenUri = "https://oauth2.googleapis.com/token"
|
||||
clientEmail = "<SERVICE_ACCOUNT_EMAIL>"
|
||||
authenticationType = "jwt"
|
||||
defaultProject = "<GCP_PROJECT_ID>"
|
||||
universeDomain = "googleapis.com"
|
||||
privateKeyPath = "/etc/secrets/gce.pem"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**Using GCE Default Service Account authentication:**
|
||||
|
||||
```hcl
|
||||
resource "grafana_data_source" "google_cloud_monitoring" {
|
||||
type = "stackdriver"
|
||||
name = "Google Cloud Monitoring"
|
||||
|
||||
json_data_encoded = jsonencode({
|
||||
authenticationType = "gce"
|
||||
universeDomain = "googleapis.com"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For all available configuration options, refer to the [Grafana provider data source resource documentation](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source).
|
||||
|
||||
## Next steps
|
||||
|
||||
After you configure the Google Cloud Monitoring data source, you can:
|
||||
|
||||
- [Query GCP metrics](ref:query-editor) using the visual Builder, MQL, SLO, or PromQL query types.
|
||||
- [Create template variables](ref:template-variables) for dynamic, reusable dashboards.
|
||||
- [Add annotations](ref:annotations) to overlay GCP events on your graphs.
|
||||
- [Set up alerting](ref:alerting) to receive notifications based on GCP metrics and SLOs.
|
||||
- [Explore your data](ref:explore) to investigate metrics without building a dashboard.
|
||||
- [Import pre-configured dashboards](ref:gcm-index) for popular GCP services.
|
||||
@@ -1,75 +1,157 @@
|
||||
---
|
||||
aliases:
|
||||
- /docs/grafana/next/datasources/cloudmonitoring/
|
||||
description: Google authentication
|
||||
- ../../data-sources/google-cloud-monitoring/google-authentication/
|
||||
description: Configure authentication methods to connect Grafana to Google Cloud Monitoring
|
||||
keywords:
|
||||
- grafana
|
||||
- google
|
||||
- cloud
|
||||
- monitoring
|
||||
- authentication
|
||||
- service account
|
||||
- jwt
|
||||
- gce
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
title: Authentication
|
||||
weight: 5
|
||||
menuTitle: Google authentication
|
||||
title: Google authentication
|
||||
weight: 200
|
||||
refs:
|
||||
configure-gcm:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/configure/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/configure/
|
||||
---
|
||||
|
||||
# Configure Google authentication
|
||||
# Google authentication
|
||||
|
||||
Requests from a Grafana plugin to Google are made on behalf of an Identity and Access Management (IAM) role or IAM user.
|
||||
The IAM user or IAM role must have the associated policies to perform certain API actions.
|
||||
Since these policies are specific to each data source, refer to the data source documentation for details.
|
||||
This document explains how to configure authentication between Grafana and Google Cloud Platform (GCP). You must configure authentication before you can use the Google Cloud Monitoring data source to query metrics and SLOs.
|
||||
|
||||
All requests to Google APIs are performed on the server-side by the Grafana backend.
|
||||
You can authenticate a Grafana plugin to Google by uploading a Google JSON Web Token (JWT) file, or by automatically retrieving credentials from the Google metadata server.
|
||||
The latter option is available only when running Grafana on a GCE virtual machine.
|
||||
|
||||
## Before you begin
|
||||
|
||||
Before you configure authentication, ensure you have the following:
|
||||
|
||||
- A Google Cloud Platform project with the Monitoring API enabled.
|
||||
- Permissions to create service accounts or configure GCE instance settings in your GCP project.
|
||||
- Access to the Grafana data source configuration page.
|
||||
|
||||
## Supported authentication methods
|
||||
|
||||
The Google Cloud Monitoring data source supports the following authentication methods:
|
||||
|
||||
| Method | Use case |
|
||||
| --------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| **Google JWT File** | Use when Grafana runs outside of GCP, or when you need explicit control over credentials. |
|
||||
| **GCE Default Service Account** | Use when Grafana runs on a Google Compute Engine VM with a configured service account. |
|
||||
| **Service account impersonation** | Use when you need Grafana to act as a different service account than the one it authenticates with. |
|
||||
|
||||
## Use a Google Service Account key file
|
||||
|
||||
To authenticate the Grafana plugin with the Google API, create a Google Cloud Platform (GCP) Service Account for the Project you want to show data.
|
||||
Use this method when Grafana runs outside of Google Cloud Platform, or when you need explicit control over which credentials are used.
|
||||
|
||||
Each Grafana data source integrates with one GCP Project.
|
||||
To visualize data from multiple GCP Projects, create one data source per GCP Project.
|
||||
Each Grafana data source connects to one GCP project by default. To visualize data from multiple GCP projects, create one data source per project, or use service account impersonation.
|
||||
|
||||
### Create a GCP Service Account and key file
|
||||
|
||||
1. Navigate to the [APIs and Services Credentials page](https://console.cloud.google.com/apis/credentials).
|
||||
1. Click on the **Create credentials** dropdown and select the **Service account** option.
|
||||
To create a service account and download its key file:
|
||||
|
||||
1. Navigate to the [APIs and Services Credentials page](https://console.cloud.google.com/apis/credentials) in the GCP Console.
|
||||
1. Click the **Create credentials** dropdown and select **Service account**.
|
||||
1. In **Service account name**, enter a name for the account.
|
||||
1. From the **Role** dropdown, choose the roles required by the specific plugin.
|
||||
1. Click **Done**.
|
||||
1. Use the newly created account to [create a service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-console).
|
||||
A JSON key file is created and downloaded to your computer.
|
||||
1. Store the key file in a secure place, because it grants access to your Google data.
|
||||
1. In the Grafana data source configuration page, upload the key file.
|
||||
The file's contents are encrypted and saved in the Grafana database.
|
||||
Remember to save the file after uploading.
|
||||
1. Click **Create and continue**.
|
||||
1. In the **Grant this service account access to project** section, select the **Monitoring Viewer** role from the **Role** dropdown.
|
||||
1. Click **Continue**, then click **Done**.
|
||||
1. In the service accounts list, click the service account you created.
|
||||
1. Go to the **Keys** tab and click **Add key** > **Create new key**.
|
||||
1. Select **JSON** and click **Create**.
|
||||
|
||||
#### Create a GCP service account for multiple projects
|
||||
A JSON key file downloads to your computer.
|
||||
|
||||
You can create a service account and key file that can be used to access multiple projects. Follow steps 1-5 above, then:
|
||||
1. Store the key file securely. It grants access to your Google Cloud data.
|
||||
|
||||
1. Note the email address of the service account, it will look a little strange like `foobar-478@main-boardwalk-90210.iam.gserviceaccount.com`.
|
||||
1. Navigate to the other project(s) you want to access.
|
||||
1. Add the service account email address to the IAM page of each project, and grant it the required roles.
|
||||
1. Navigate back to the original project's service account and create a [service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys#iam-service-account-keys-create-console). A JSON key file is created and downloaded to your computer
|
||||
1. Store the key file in a secure place, because it grants access to your Google data.
|
||||
1. In the Grafana data source configuration page, upload the key file.
|
||||
The file's contents are encrypted and saved in the Grafana database.
|
||||
Remember to save the file after uploading.
|
||||
### Upload the key file to Grafana
|
||||
|
||||
## Configure a GCE default service account
|
||||
1. In Grafana, navigate to the Google Cloud Monitoring data source configuration page.
|
||||
1. Under **Authentication type**, select **Google JWT File**.
|
||||
1. Upload the JSON key file using one of the available methods (drag and drop, browse, or paste).
|
||||
1. Click **Save & test** to verify the connection.
|
||||
|
||||
When Grafana is running on a Google Compute Engine (GCE) virtual machine, Grafana can automatically retrieve default credentials from the metadata server. As a result, there is no need to generate a private key file for the service account. You also do not need to upload the file to Grafana. The following preconditions must be met before Grafana can retrieve default credentials.
|
||||
### Grant access to multiple projects
|
||||
|
||||
- You must create a Service Account for use by the GCE virtual machine. For more information, refer to [Create new service account](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createanewserviceaccount).
|
||||
- Verify that the GCE virtual machine instance is running as the service account that you created. For more information, refer to [setting up an instance to run as a service account](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using).
|
||||
- Allow access to the specified API scope.
|
||||
You can configure a single service account to access multiple GCP projects:
|
||||
|
||||
For more information about creating and enabling service accounts for GCE instances, refer to [enabling service accounts for instances in Google documentation](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances).
|
||||
1. Create a service account and key file following the steps above.
|
||||
1. Note the service account email address (for example, `grafana-monitoring@my-project.iam.gserviceaccount.com`).
|
||||
1. In each additional project you want to access:
|
||||
1. Navigate to **IAM & Admin** > **IAM**.
|
||||
1. Click **Grant access**.
|
||||
1. Enter the service account email address.
|
||||
1. Assign the **Monitoring Viewer** role.
|
||||
1. Click **Save**.
|
||||
1. Use the original key file in Grafana. The service account can now access all configured projects.
|
||||
|
||||
### Service account impersonation
|
||||
## Use GCE Default Service Account
|
||||
|
||||
You can also configure the plugin to use [service account impersonation](https://cloud.google.com/iam/docs/service-account-impersonation).
|
||||
You need to ensure the service account used by this plugin has the `iam.serviceAccounts.getAccessToken` permission. This permission is in roles like the [Service Account Token Creator role](https://cloud.google.com/iam/docs/roles-permissions/iam#iam.serviceAccountTokenCreator) (roles/iam.serviceAccountTokenCreator). Also, the service account impersonated by this plugin needs [Monitoring Viewer](https://cloud.google.com/iam/docs/roles-permissions/monitoring#monitoring.viewer).
|
||||
When Grafana runs on a Google Compute Engine (GCE) virtual machine, it can automatically retrieve credentials from the GCE metadata server. This method doesn't require you to create or manage key files.
|
||||
|
||||
### Prerequisites for GCE authentication
|
||||
|
||||
Before using this method, ensure the following:
|
||||
|
||||
- Grafana is running on a GCE virtual machine.
|
||||
- The VM has a service account assigned with the **Monitoring Viewer** role.
|
||||
- The VM has the **Cloud Monitoring API** scope enabled.
|
||||
|
||||
### Configure the GCE instance
|
||||
|
||||
1. In the GCP Console, navigate to **Compute Engine** > **VM instances**.
|
||||
1. Stop the VM if it's running (you can't change scopes on a running VM).
|
||||
1. Click the VM name, then click **Edit**.
|
||||
1. Under **Service account**, select a service account with the **Monitoring Viewer** role.
|
||||
1. Under **Access scopes**, select **Set access for each API** and enable the **Cloud Monitoring API** (read-only).
|
||||
1. Click **Save** and restart the VM.
|
||||
|
||||
### Configure the data source
|
||||
|
||||
1. In Grafana, navigate to the Google Cloud Monitoring data source configuration page.
|
||||
1. Under **Authentication type**, select **GCE Default Service Account**.
|
||||
1. Click **Save & test** to verify the connection.
|
||||
|
||||
For more information about GCE service accounts, refer to the [Google documentation on service accounts for instances](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances).
|
||||
|
||||
## Configure service account impersonation
|
||||
|
||||
Service account impersonation allows Grafana to authenticate as one service account but act as a different service account when making API requests. This is useful for:
|
||||
|
||||
- Accessing resources across multiple projects with a single configuration.
|
||||
- Following the principle of least privilege by separating authentication from authorization.
|
||||
- Auditing and tracking which service account accessed specific resources.
|
||||
|
||||
### Prerequisites for impersonation
|
||||
|
||||
The service account used by Grafana (the "caller") must have the following:
|
||||
|
||||
- The `iam.serviceAccounts.getAccessToken` permission on the target service account.
|
||||
- This permission is included in the **Service Account Token Creator** role (`roles/iam.serviceAccountTokenCreator`).
|
||||
|
||||
The service account being impersonated (the "target") must have:
|
||||
|
||||
- The **Monitoring Viewer** role on the projects you want to access.
|
||||
|
||||
### Configure impersonation
|
||||
|
||||
1. In the GCP Console, grant the caller service account the **Service Account Token Creator** role on the target service account.
|
||||
1. Grant the target service account the **Monitoring Viewer** role on the relevant projects.
|
||||
1. In Grafana, navigate to the Google Cloud Monitoring data source configuration page.
|
||||
1. Configure authentication using either **Google JWT File** or **GCE Default Service Account**.
|
||||
1. Enable **Service account impersonation**.
|
||||
1. Enter the email address of the target service account.
|
||||
1. Click **Save & test** to verify the connection.
|
||||
|
||||
For more information, refer to the [Google documentation on service account impersonation](https://cloud.google.com/iam/docs/service-account-impersonation).
|
||||
|
||||
@@ -39,17 +39,16 @@ refs:
|
||||
This topic explains querying specific to the Google Cloud Monitoring data source.
|
||||
For general documentation on querying data sources in Grafana, see [Query and transform data](ref:query-transform-data).
|
||||
|
||||
## Choose a query editing mode
|
||||
## Query types
|
||||
|
||||
The Google Cloud Monitoring query editor helps you build queries for two types of data, which both return time series data:
|
||||
The Google Cloud Monitoring query editor supports the following query types:
|
||||
|
||||
- [Metrics](#query-metrics)
|
||||
|
||||
You can also create [Monitoring Query Language (MQL)](#use-the-monitoring-query-language) queries.
|
||||
|
||||
- [Service Level Objectives (SLO)](#query-service-level-objectives)
|
||||
|
||||
You also use the query editor when you [annotate](#apply-annotations) visualizations.
|
||||
| Query type | Description |
|
||||
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| [**Builder**](#query-metrics) | Build metrics queries visually by selecting a service, metric, filters, and aggregation options. |
|
||||
| [**MQL**](#use-the-monitoring-query-language) | Write queries using the Monitoring Query Language for advanced use cases. |
|
||||
| [**Service Level Objectives (SLO)**](#query-service-level-objectives) | Query SLO data to track service reliability and error budgets. |
|
||||
| [**PromQL**](#query-with-promql) | Write Prometheus-style queries against Google Cloud Monitoring metrics. |
|
||||
|
||||
## Query metrics
|
||||
|
||||
@@ -59,7 +58,7 @@ The metrics query editor helps you select metrics, group and aggregate by labels
|
||||
|
||||
### Create a metrics query
|
||||
|
||||
1. Select the **Metrics** option in the **Query Type** dropdown.
|
||||
1. Select **Builder** in the **Query type** dropdown.
|
||||
1. Select a project from the **Project** dropdown.
|
||||
1. Select a Google Cloud Platform service from the **Service** dropdown.
|
||||
1. Select a metric from the **Metric** dropdown.
|
||||
@@ -234,9 +233,21 @@ To understand basic MQL concepts, refer to [Introduction to Monitoring Query Lan
|
||||
|
||||
**To create an MQL query:**
|
||||
|
||||
1. Select the **Metrics** option in the **Query Type** dropdown.
|
||||
1. Select **MQL** in the **Query type** dropdown.
|
||||
1. Select a project from the **Project** dropdown.
|
||||
1. Enter your MQL query in the text area.
|
||||
1. _(Optional)_ Configure the **Graph period** setting.
|
||||
|
||||
Press `Shift+Enter` to run the query.
|
||||
|
||||
#### Configure MQL options
|
||||
|
||||
The following options are available for MQL queries:
|
||||
|
||||
| Setting | Description |
|
||||
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Alias by** | Control the format of legend keys. Refer to [Set alias patterns for MQL queries](#set-alias-patterns-for-mql-queries) for available patterns. |
|
||||
| **Graph period** | Enable the toggle to override the default time period. Select a period from the dropdown to control the granularity of the returned time series data. |
|
||||
|
||||
### Set alias patterns for MQL queries
|
||||
|
||||
@@ -255,7 +266,7 @@ To understand basic concepts in service monitoring, refer to the [Google Cloud M
|
||||
|
||||
**To create an SLO query:**
|
||||
|
||||
1. Select the **Service Level Objectives (SLO)** option in the **Query Type** dropdown.
|
||||
1. Select **Service Level Objectives (SLO)** in the **Query type** dropdown.
|
||||
1. Select a project from the **Project** dropdown.
|
||||
1. Select an [SLO service](https://cloud.google.com/monitoring/api/ref_v3/rest/v3/services) from the **Service** dropdown.
|
||||
1. Select an [SLO](https://cloud.google.com/monitoring/api/ref_v3/rest/v3/services.serviceLevelObjectives) from the **SLO** dropdown.
|
||||
@@ -285,41 +296,46 @@ The **Alias By** field helps you control the format of legend keys for SLO queri
|
||||
|
||||
SLO queries use the same alignment period functionality as [metric queries](#define-the-alignment-period).
|
||||
|
||||
### Create a Prometheus query
|
||||
## Query with PromQL
|
||||
|
||||
**To create an Prometheus query:**
|
||||
The PromQL query type allows you to query Google Cloud Monitoring metrics using Prometheus Query Language (PromQL) syntax. This is useful if you're familiar with PromQL from Prometheus or Grafana Mimir and want to use the same query syntax with Google Cloud Monitoring data.
|
||||
|
||||
1. Select the **PromQL** option in the **Query Type** dropdown.
|
||||
For more information about PromQL support in Google Cloud Monitoring, refer to the [Google Cloud documentation on PromQL](https://cloud.google.com/monitoring/promql).
|
||||
|
||||
### Create a PromQL query
|
||||
|
||||
To create a PromQL query:
|
||||
|
||||
1. Select **PromQL** in the **Query type** dropdown.
|
||||
1. Select a project from the **Project** dropdown.
|
||||
1. Enter your Prometheus query in the text area.
|
||||
1. Enter a Min Step interval. The **Min step** setting defines the lower bounds on the interval between data points. For example, set this to `1h` to hint that measurements are taken hourly. This setting supports the `$__interval` and `$__rate_interval` macros.
|
||||
1. Enter your PromQL query in the text area.
|
||||
|
||||
## Apply annotations
|
||||
### Configure PromQL options
|
||||
|
||||
{{< figure src="/static/img/docs/google-cloud-monitoring/annotations-8-0.png" max-width= "400px" class="docs-image--right" >}}
|
||||
The following options are available for PromQL queries:
|
||||
|
||||
[Annotations](ref:annotate-visualizations) overlay rich event information on top of graphs.
|
||||
You can add annotation queries in the Dashboard menu's Annotations view.
|
||||
| Setting | Description |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Min step** | Defines the lower bounds on the interval between data points. For example, set this to `1h` to hint that measurements are taken hourly. Supports the `$__interval` and `$__rate_interval` macros. |
|
||||
|
||||
Rendering annotations is expensive, and it's important to limit the number of rows returned.
|
||||
There's no support for displaying Google Cloud Monitoring's annotations and events, but it works well with [custom metrics](https://cloud.google.com/monitoring/custom-metrics/) in Google Cloud Monitoring.
|
||||
### PromQL query examples
|
||||
|
||||
With the query editor for annotations, you can select a metric and filters.
|
||||
The `Title` and `Text` fields support templating and can use data returned from the query.
|
||||
The following examples show common PromQL query patterns for Google Cloud Monitoring:
|
||||
|
||||
For example, the Title field could have the following text:
|
||||
**Query CPU utilization for Compute Engine instances:**
|
||||
|
||||
`{{metric.type}} has value: {{metric.value}}`
|
||||
```promql
|
||||
compute_googleapis_com:instance_cpu_utilization
|
||||
```
|
||||
|
||||
Example result: `monitoring.googleapis.com/uptime_check/http_status has this value: 502`
|
||||
**Filter by label:**
|
||||
|
||||
### Patterns for the annotation query editor
|
||||
```promql
|
||||
compute_googleapis_com:instance_cpu_utilization{instance_name="my-instance"}
|
||||
```
|
||||
|
||||
| Alias pattern format | Description | Alias pattern example | Example result |
|
||||
| ------------------------ | --------------------------------- | -------------------------------- | ------------------------------------------------- |
|
||||
| `{{metric.value}}` | Value of the metric/point. | `{{metric.value}}` | `555` |
|
||||
| `{{metric.type}}` | Returns the full Metric Type. | `{{metric.type}}` | `compute.googleapis.com/instance/cpu/utilization` |
|
||||
| `{{metric.name}}` | Returns the metric name part. | `{{metric.name}}` | `instance/cpu/utilization` |
|
||||
| `{{metric.service}}` | Returns the service part. | `{{metric.service}}` | `compute` |
|
||||
| `{{metric.label.xxx}}` | Returns the metric label value. | `{{metric.label.instance_name}}` | `grafana-1-prod` |
|
||||
| `{{resource.label.xxx}}` | Returns the resource label value. | `{{resource.label.zone}}` | `us-east1-b` |
|
||||
**Calculate the rate of a counter metric:**
|
||||
|
||||
```promql
|
||||
rate(logging_googleapis_com:log_entry_count[5m])
|
||||
```
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
---
|
||||
aliases:
|
||||
- ../../data-sources/google-cloud-monitoring/troubleshooting/
|
||||
description: Troubleshooting guide for the Google Cloud Monitoring data source in Grafana
|
||||
keywords:
|
||||
- grafana
|
||||
- google
|
||||
- cloud
|
||||
- monitoring
|
||||
- stackdriver
|
||||
- troubleshooting
|
||||
- errors
|
||||
- authentication
|
||||
- query
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
- oss
|
||||
menuTitle: Troubleshooting
|
||||
title: Troubleshoot Google Cloud Monitoring data source issues
|
||||
weight: 500
|
||||
refs:
|
||||
configure-gcm:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/configure/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/configure/
|
||||
google-authentication:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/google-authentication/
|
||||
template-variables:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/template-variables/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/template-variables/
|
||||
query-editor:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/query-editor/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/datasources/google-cloud-monitoring/query-editor/
|
||||
private-data-source-connect:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/connect-externally-hosted/private-data-source-connect/
|
||||
---
|
||||
|
||||
# Troubleshoot Google Cloud Monitoring data source issues
|
||||
|
||||
This document provides solutions to common issues you may encounter when configuring or using the Google Cloud Monitoring data source. For configuration instructions, refer to [Configure Google Cloud Monitoring](ref:configure-gcm).
|
||||
|
||||
## Authentication errors
|
||||
|
||||
These errors occur when GCP credentials are invalid, missing, or don't have the required permissions.
|
||||
|
||||
### "Permission denied" or "Access denied"
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Save & test fails with permission errors
|
||||
- Queries return authorization errors
|
||||
- Projects, metrics, or labels don't load
|
||||
|
||||
**Possible causes and solutions:**
|
||||
|
||||
| Cause | Solution |
|
||||
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Service account missing required permissions | Assign the **Monitoring Viewer** role to the service account in the GCP Console under **IAM & Admin** > **IAM**. Refer to [Configure Google Cloud Monitoring](ref:configure-gcm) for details. |
|
||||
| Incorrect service account key file | Verify the JSON key file was downloaded correctly and contains valid credentials. Generate a new key if necessary. |
|
||||
| Service account key has been deleted | Check the service account in GCP Console under **IAM & Admin** > **Service Accounts**. If the key was deleted, create a new one. |
|
||||
| Wrong project selected | Verify the default project in the data source configuration matches a project the service account has access to. |
|
||||
| APIs not enabled | Enable the Monitoring API and Cloud Resource Manager API in the GCP Console. Refer to [Configure Google Cloud Monitoring](ref:configure-gcm) for links. |
|
||||
|
||||
### "Invalid JWT" or "JWT token error"
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Authentication fails when using Google JWT File
|
||||
- Error message references invalid or malformed JWT
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify you uploaded the complete JSON key file, not just the private key portion.
|
||||
1. Check that the JSON file is properly formatted and not corrupted.
|
||||
1. Ensure the key file contains all required fields: `type`, `project_id`, `private_key_id`, `private_key`, `client_email`, `client_id`, `auth_uri`, `token_uri`.
|
||||
1. Generate a new service account key and re-upload it.
|
||||
|
||||
### GCE Default Service Account not working
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Data source test fails when using GCE Default Service Account
|
||||
- Works with JWT but fails with GCE authentication
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify Grafana is running on a Google Compute Engine (GCE) virtual machine.
|
||||
1. Check that the GCE instance has the **Cloud Monitoring API** scope enabled.
|
||||
1. Verify the GCE default service account has the **Monitoring Viewer** role.
|
||||
1. If the VM was created without the required scope, you may need to stop the instance, edit it to add the scope, and restart.
|
||||
|
||||
### Service account impersonation errors
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Authentication fails when service account impersonation is enabled
|
||||
- Error: "Unable to impersonate service account"
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the primary service account has the `roles/iam.serviceAccountTokenCreator` role on the target service account.
|
||||
1. Check that the target service account email is entered correctly.
|
||||
1. Ensure the target service account has the **Monitoring Viewer** role.
|
||||
1. Verify both service accounts are in projects that have the required APIs enabled.
|
||||
|
||||
## Connection errors
|
||||
|
||||
These errors occur when Grafana cannot reach Google Cloud Monitoring endpoints.
|
||||
|
||||
### "Request timed out" or connection failures
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Data source test times out
|
||||
- Queries fail with timeout errors
|
||||
- Intermittent connection issues
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify network connectivity from the Grafana server to Google Cloud endpoints (`monitoring.googleapis.com`).
|
||||
1. Check firewall rules allow outbound HTTPS (port 443) to Google Cloud services.
|
||||
1. For Grafana Cloud connecting to private resources, configure [Private data source connect](ref:private-data-source-connect).
|
||||
1. Check if a corporate proxy is blocking connections to Google Cloud.
|
||||
|
||||
### "SSL certificate problem"
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- SSL/TLS handshake errors
|
||||
- Certificate verification failures
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Ensure the system time is correct on the Grafana server.
|
||||
1. Verify the Grafana server has up-to-date CA certificates installed.
|
||||
1. Check if a corporate proxy is intercepting HTTPS traffic.
|
||||
|
||||
## Metrics query errors
|
||||
|
||||
These errors occur when querying Google Cloud Monitoring metrics.
|
||||
|
||||
### "No data" or empty results
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Query executes without error but returns no data
|
||||
- Charts show "No data" message
|
||||
|
||||
**Possible causes and solutions:**
|
||||
|
||||
| Cause | Solution |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| Time range doesn't contain data | Expand the dashboard time range. GCP metrics have different retention periods based on the metric type. |
|
||||
| Wrong project selected | Verify you've selected the correct project in the query editor. |
|
||||
| Incorrect metric type | Verify the service, metric type, and metric are correct. Check available metrics in the GCP Console Metrics Explorer. |
|
||||
| Missing labels/filters | Some metrics require specific label filters to return data. Try removing filters to see if data appears. |
|
||||
| Resource not emitting metrics | Verify the resource exists and is actively emitting metrics. Some metrics only populate under certain conditions. |
|
||||
|
||||
### Metrics don't appear in drop-down
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Expected metrics don't appear in the query editor
|
||||
- Metric drop-down is empty for a service
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the metric exists in the selected project and region.
|
||||
1. Check that the service account has the **Monitoring Viewer** role.
|
||||
1. Some metrics are only available for specific resource types. Check the [Google Cloud metrics list](https://cloud.google.com/monitoring/api/metrics_gcp).
|
||||
1. Use the Query Inspector to verify the API request and response.
|
||||
|
||||
### Label values not loading
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Label value drop-down doesn't populate
|
||||
- Filters can't be applied
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the service account has the **Monitoring Viewer** role.
|
||||
1. Ensure a project, service, and metric are selected before label values can load.
|
||||
1. Label values are populated from existing metric data. If no metrics match the current selection, no values appear.
|
||||
|
||||
### MQL query errors
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Monitoring Query Language (MQL) queries fail with syntax errors
|
||||
- MQL query returns unexpected results
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Validate your MQL syntax using the [MQL reference documentation](https://cloud.google.com/monitoring/mql/reference).
|
||||
1. Check for typos in metric types, label names, or function names.
|
||||
1. Ensure time range syntax is valid in your query.
|
||||
1. Test the query in the GCP Console Metrics Explorer before using it in Grafana.
|
||||
|
||||
### "Too many data points" or API throttling
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Queries fail with quota errors
|
||||
- Performance degrades with multiple panels
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Increase the alignment period to reduce the number of data points.
|
||||
1. Reduce the time range of your queries.
|
||||
1. Use fewer metric queries per panel.
|
||||
1. Request a quota increase in the GCP Console under **APIs & Services** > **Quotas**.
|
||||
1. Enable query caching in Grafana to reduce API calls.
|
||||
|
||||
## SLO query errors
|
||||
|
||||
These errors are specific to Service Level Objective (SLO) queries.
|
||||
|
||||
### SLO services don't appear
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- SLO service selector is empty
|
||||
- Can't find expected SLO services
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify SLOs are defined in Google Cloud Monitoring for the selected project.
|
||||
1. Check that the service account has access to view SLOs.
|
||||
1. Ensure the project has services configured in the Service Monitoring section of the GCP Console.
|
||||
|
||||
### SLO query returns no data
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- SLO query executes but returns no data
|
||||
- SLO values show as empty
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the SLO exists and is active in the GCP Console.
|
||||
1. Check that the time range includes periods when the SLO had data.
|
||||
1. Ensure the selected SLO selector (SLI value, compliance, error budget, etc.) is appropriate for the SLO type.
|
||||
1. Some SLOs may not have data if the underlying service hasn't received traffic.
|
||||
|
||||
## Template variable errors
|
||||
|
||||
These errors occur when using template variables with the Google Cloud Monitoring data source.
|
||||
|
||||
### Variables return no values
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Variable drop-down is empty
|
||||
- Dashboard fails to load with variable errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the data source connection is working.
|
||||
1. Check that the service account has permissions to list the requested resources.
|
||||
1. For dependent variables, ensure parent variables have valid selections.
|
||||
1. Verify the project is selected correctly in the variable query.
|
||||
|
||||
### Variables are slow to load
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Dashboard takes a long time to load
|
||||
- Variable selectors are slow to populate
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Set variable refresh to **On dashboard load** instead of **On time range change**.
|
||||
1. Reduce the scope of variable queries (filter by specific project or service).
|
||||
1. Limit the number of dependent variables in a chain.
|
||||
|
||||
For more information on template variables, refer to the [template variables documentation](ref:template-variables).
|
||||
|
||||
## Pre-configured dashboard issues
|
||||
|
||||
These issues occur with the bundled pre-configured dashboards.
|
||||
|
||||
### Imported dashboards show no data
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Imported dashboards show empty panels
|
||||
- Template variables don't load
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify the data source name in the dashboard matches your Google Cloud Monitoring data source.
|
||||
1. Check that the service account has access to the projects shown in the project variable.
|
||||
1. Ensure the resources (Compute Engine instances, Cloud SQL, etc.) exist and are emitting metrics.
|
||||
1. Verify the required GCP services are enabled in your project.
|
||||
|
||||
## Enable debug logging
|
||||
|
||||
To capture detailed error information for troubleshooting:
|
||||
|
||||
1. Set the Grafana log level to `debug` in the configuration file:
|
||||
|
||||
```ini
|
||||
[log]
|
||||
level = debug
|
||||
```
|
||||
|
||||
1. Review logs in `/var/log/grafana/grafana.log` (or your configured log location).
|
||||
1. Look for Google Cloud Monitoring-specific entries that include request and response details.
|
||||
1. Reset the log level to `info` after troubleshooting to avoid excessive log volume.
|
||||
|
||||
## Get additional help
|
||||
|
||||
If you've tried the solutions above and still encounter issues:
|
||||
|
||||
1. Check the [Grafana community forums](https://community.grafana.com/) for similar issues.
|
||||
1. Review the [Google Cloud Monitoring plugin GitHub issues](https://github.com/grafana/grafana/issues) for known bugs.
|
||||
1. Consult the [Google Cloud Monitoring documentation](https://cloud.google.com/monitoring/docs) for service-specific guidance.
|
||||
1. Contact Grafana Support if you're an Enterprise, Cloud Pro, or Cloud Contracted user.
|
||||
1. When reporting issues, include:
|
||||
- Grafana version
|
||||
- GCP project (redact if sensitive)
|
||||
- Error messages (redact sensitive information)
|
||||
- Steps to reproduce
|
||||
- Query configuration (redact credentials)
|
||||
@@ -72,8 +72,6 @@ Each panel needs at least one query to display a visualization.
|
||||
|
||||
## Create a dashboard
|
||||
|
||||
{{< docs/list >}}
|
||||
{{< shared id="create-dashboard" >}}
|
||||
To create a dashboard, follow these steps:
|
||||
|
||||
1. Click **Dashboards** in the main menu.
|
||||
|
||||
@@ -146,7 +146,7 @@ To create a variable, follow these steps:
|
||||
- Variable drop-down lists are displayed in the order in which they're listed in the **Variables** in dashboard settings, so put the variables that you will change often at the top, so they will be shown first (far left on the dashboard).
|
||||
- By default, variables don't have a default value. This means that the topmost value in the drop-down list is always preselected. If you want to pre-populate a variable with an empty value, you can use the following workaround in the variable settings:
|
||||
1. Select the **Include All Option** checkbox.
|
||||
2. In the **Custom all value** field, enter a value like `+`.
|
||||
2. In the **Custom all value** field, enter a value like `.+`.
|
||||
|
||||
## Add a query variable
|
||||
|
||||
|
||||
@@ -689,9 +689,6 @@ func validateListHistoryRequest(req *resourcepb.ListRequest) error {
|
||||
if key.Namespace == "" {
|
||||
return fmt.Errorf("namespace is required")
|
||||
}
|
||||
if key.Name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
||||
sqldb "github.com/grafana/grafana/pkg/storage/unified/sql/db"
|
||||
@@ -99,6 +100,10 @@ func RunStorageBackendTest(t *testing.T, newBackend NewBackendFunc, opts *TestOp
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if db.IsTestDbSQLite() {
|
||||
t.Skip("Skipping tests on sqlite until channel notifier is implemented")
|
||||
}
|
||||
|
||||
tc.fn(t, newBackend(context.Background()), opts.NSPrefix)
|
||||
})
|
||||
}
|
||||
@@ -1166,7 +1171,7 @@ func runTestIntegrationBackendCreateNewResource(t *testing.T, backend resource.S
|
||||
}))
|
||||
|
||||
server := newServer(t, backend)
|
||||
ns := nsPrefix + "-create-resource"
|
||||
ns := nsPrefix + "-create-rsrce" // create-resource
|
||||
ctx = request.WithNamespace(ctx, ns)
|
||||
|
||||
request := &resourcepb.CreateRequest{
|
||||
@@ -1607,7 +1612,7 @@ func (s *sliceBulkRequestIterator) RollbackRequested() bool {
|
||||
|
||||
func runTestIntegrationBackendOptimisticLocking(t *testing.T, backend resource.StorageBackend, nsPrefix string) {
|
||||
ctx := testutil.NewTestContext(t, time.Now().Add(30*time.Second))
|
||||
ns := nsPrefix + "-optimistic-locking"
|
||||
ns := nsPrefix + "-optimis-lock" // optimistic-locking. need to cut down on characters to not exceed namespace character limit (40)
|
||||
|
||||
t.Run("concurrent updates with same RV - only one succeeds", func(t *testing.T) {
|
||||
// Create initial resource with rv0 (no previous RV)
|
||||
|
||||
@@ -36,6 +36,10 @@ func NewTestSqlKvBackend(t *testing.T, ctx context.Context, withRvManager bool)
|
||||
KvStore: kv,
|
||||
}
|
||||
|
||||
if db.DriverName() == "sqlite3" {
|
||||
kvOpts.UseChannelNotifier = true
|
||||
}
|
||||
|
||||
if withRvManager {
|
||||
dialect := sqltemplate.DialectForDriver(db.DriverName())
|
||||
rvManager, err := rvmanager.NewResourceVersionManager(rvmanager.ResourceManagerOptions{
|
||||
|
||||
@@ -41,17 +41,9 @@ func TestIntegrationSQLKVStorageBackend(t *testing.T) {
|
||||
testutil.SkipIntegrationTestInShortMode(t)
|
||||
|
||||
skipTests := map[string]bool{
|
||||
TestWatchWriteEvents: true,
|
||||
TestList: true,
|
||||
TestBlobSupport: true,
|
||||
TestGetResourceStats: true,
|
||||
TestListHistory: true,
|
||||
TestListHistoryErrorReporting: true,
|
||||
TestListModifiedSince: true,
|
||||
TestListTrash: true,
|
||||
TestCreateNewResource: true,
|
||||
TestGetResourceLastImportTime: true,
|
||||
TestOptimisticLocking: true,
|
||||
}
|
||||
|
||||
t.Run("Without RvManager", func(t *testing.T) {
|
||||
@@ -59,7 +51,7 @@ func TestIntegrationSQLKVStorageBackend(t *testing.T) {
|
||||
backend, _ := NewTestSqlKvBackend(t, ctx, false)
|
||||
return backend
|
||||
}, &TestOptions{
|
||||
NSPrefix: "sqlkvstorage-test",
|
||||
NSPrefix: "sqlkvstoragetest",
|
||||
SkipTests: skipTests,
|
||||
})
|
||||
})
|
||||
@@ -69,7 +61,7 @@ func TestIntegrationSQLKVStorageBackend(t *testing.T) {
|
||||
backend, _ := NewTestSqlKvBackend(t, ctx, true)
|
||||
return backend
|
||||
}, &TestOptions{
|
||||
NSPrefix: "sqlkvstorage-withrvmanager-test",
|
||||
NSPrefix: "sqlkvstoragetest-rvmanager",
|
||||
SkipTests: skipTests,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
"github.com/grafana/alerting/notify"
|
||||
"github.com/grafana/alerting/receivers/schema"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alertingnotifications/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/tests/api/alerting"
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
)
|
||||
|
||||
@@ -34,7 +33,8 @@ func TestIntegrationReadImported_Snapshot(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
receiverClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
receiverClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||
alertingApi := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
|
||||
@@ -58,9 +58,9 @@ func TestIntegrationReadImported_Snapshot(t *testing.T) {
|
||||
response := alertingApi.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
require.Equal(t, "success", response.Status)
|
||||
|
||||
receiversRaw, err := receiverClient.Client.List(ctx, v1.ListOptions{})
|
||||
receiversRaw, err := receiverClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
raw, err := receiversRaw.MarshalJSON()
|
||||
raw, err := json.Marshal(receiversRaw)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedBytes, err := os.ReadFile(path.Join("test-data", "imported-expected-snapshot.json"))
|
||||
@@ -74,7 +74,7 @@ func TestIntegrationReadImported_Snapshot(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
receivers, err := receiverClient.List(ctx, v1.ListOptions{})
|
||||
receivers, err := receiverClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
t.Run("secure fields should be properly masked", func(t *testing.T) {
|
||||
for _, receiver := range receivers.Items {
|
||||
@@ -114,14 +114,14 @@ func TestIntegrationReadImported_Snapshot(t *testing.T) {
|
||||
toUpdate := receivers.Items[1]
|
||||
toUpdate.Spec.Title = "another title"
|
||||
|
||||
_, err = receiverClient.Update(ctx, &toUpdate, v1.UpdateOptions{})
|
||||
_, err = receiverClient.Update(ctx, &toUpdate, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should not be able to delete", func(t *testing.T) {
|
||||
toDelete := receivers.Items[1]
|
||||
|
||||
err = receiverClient.Delete(ctx, toDelete.Name, v1.DeleteOptions{})
|
||||
err = receiverClient.Delete(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: toDelete.Name}, resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ import (
|
||||
"github.com/grafana/alerting/notify/notifytest"
|
||||
"github.com/grafana/alerting/receivers/line"
|
||||
"github.com/grafana/alerting/receivers/schema"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/grafana/alerting/notify"
|
||||
|
||||
@@ -65,7 +65,8 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
client := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
client, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
newResource := &v0alpha1.Receiver{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
@@ -77,42 +78,42 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("create should fail if object name is specified", func(t *testing.T) {
|
||||
resource := newResource.Copy().(*v0alpha1.Receiver)
|
||||
resource.Name = "new-receiver"
|
||||
_, err := client.Create(ctx, resource, v1.CreateOptions{})
|
||||
receiver := newResource.Copy().(*v0alpha1.Receiver)
|
||||
receiver.Name = "new-receiver"
|
||||
_, err := client.Create(ctx, receiver, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
|
||||
})
|
||||
|
||||
var resourceID string
|
||||
var resourceID resource.Identifier
|
||||
t.Run("create should succeed and provide resource name", func(t *testing.T) {
|
||||
actual, err := client.Create(ctx, newResource, v1.CreateOptions{})
|
||||
actual, err := client.Create(ctx, newResource, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
require.NotEmptyf(t, actual.UID, "Resource UID should not be empty")
|
||||
resourceID = actual.Name
|
||||
resourceID = actual.GetStaticMetadata().Identifier()
|
||||
})
|
||||
|
||||
t.Run("resource should be available by the identifier", func(t *testing.T) {
|
||||
actual, err := client.Get(ctx, resourceID, v1.GetOptions{})
|
||||
actual, err := client.Get(ctx, resourceID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
require.Equal(t, newResource.Spec, actual.Spec)
|
||||
})
|
||||
|
||||
t.Run("update should rename receiver if name in the specification changes", func(t *testing.T) {
|
||||
existing, err := client.Get(ctx, resourceID, v1.GetOptions{})
|
||||
existing, err := client.Get(ctx, resourceID)
|
||||
require.NoError(t, err)
|
||||
|
||||
updated := existing.Copy().(*v0alpha1.Receiver)
|
||||
updated.Spec.Title = "another-newReceiver"
|
||||
|
||||
actual, err := client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actual, err := client.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, updated.Spec, actual.Spec)
|
||||
require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
|
||||
require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't")
|
||||
|
||||
resource, err := client.Get(ctx, actual.Name, v1.GetOptions{})
|
||||
resource, err := client.Get(ctx, actual.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, actual.Spec, resource.Spec)
|
||||
require.Equal(t, actual.Name, resource.Name)
|
||||
@@ -140,7 +141,8 @@ func TestIntegrationResourcePermissions(t *testing.T) {
|
||||
admin := org1.Admin
|
||||
viewer := org1.Viewer
|
||||
editor := org1.Editor
|
||||
adminClient := test_common.NewReceiverClient(t, admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
writeACMetadata := []string{"canWrite", "canDelete"}
|
||||
allACMetadata := []string{"canWrite", "canDelete", "canReadSecrets", "canAdmin", "canModifyProtected"}
|
||||
@@ -292,8 +294,10 @@ func TestIntegrationResourcePermissions(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
createClient := test_common.NewReceiverClient(t, tc.creatingUser)
|
||||
client := test_common.NewReceiverClient(t, tc.testUser)
|
||||
createClient, err := v0alpha1.NewReceiverClientFromGenerator(tc.creatingUser.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
client, err := v0alpha1.NewReceiverClientFromGenerator(tc.testUser.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
var created = &v0alpha1.Receiver{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -308,12 +312,12 @@ func TestIntegrationResourcePermissions(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create receiver with creatingUser
|
||||
created, err = createClient.Create(ctx, created, v1.CreateOptions{})
|
||||
created, err = createClient.Create(ctx, created, resource.CreateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
require.NotNil(t, created)
|
||||
|
||||
defer func() {
|
||||
_ = adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
|
||||
_ = adminClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
}()
|
||||
|
||||
// Assign resource permissions
|
||||
@@ -338,7 +342,7 @@ func TestIntegrationResourcePermissions(t *testing.T) {
|
||||
|
||||
// Obtain expected responses using admin client as source of truth.
|
||||
expectedGetWithMetadata, expectedListWithMetadata := func() (*v0alpha1.Receiver, *v0alpha1.Receiver) {
|
||||
expectedGet, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
expectedGet, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, expectedGet)
|
||||
|
||||
@@ -352,7 +356,7 @@ func TestIntegrationResourcePermissions(t *testing.T) {
|
||||
expectedGetWithMetadata.SetAccessControl(ac)
|
||||
}
|
||||
|
||||
expectedList, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
expectedList, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
expectedListWithMetadata := extractReceiverFromList(expectedList, created.Name)
|
||||
require.NotNil(t, expectedListWithMetadata)
|
||||
@@ -368,26 +372,26 @@ func TestIntegrationResourcePermissions(t *testing.T) {
|
||||
}()
|
||||
|
||||
t.Run("should be able to list receivers", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
listedReceiver := extractReceiverFromList(list, created.Name)
|
||||
assert.Equalf(t, expectedListWithMetadata, listedReceiver, "Expected %v but got %v", expectedListWithMetadata, listedReceiver)
|
||||
})
|
||||
|
||||
t.Run("should be able to read receiver by resource identifier", func(t *testing.T) {
|
||||
got, err := client.Get(ctx, expectedGetWithMetadata.Name, v1.GetOptions{})
|
||||
got, err := client.Get(ctx, expectedGetWithMetadata.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
assert.Equalf(t, expectedGetWithMetadata, got, "Expected %v but got %v", expectedGetWithMetadata, got)
|
||||
})
|
||||
} else {
|
||||
t.Run("list receivers should be empty", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Emptyf(t, list.Items, "Expected no receivers but got %v", list.Items)
|
||||
})
|
||||
|
||||
t.Run("should be forbidden to read receiver by name", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, created.Name, v1.GetOptions{})
|
||||
_, err := client.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
}
|
||||
@@ -559,10 +563,12 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
|
||||
client := test_common.NewReceiverClient(t, tc.user)
|
||||
client, err := v0alpha1.NewReceiverClientFromGenerator(tc.user.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
var expected = &v0alpha1.Receiver{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -580,29 +586,29 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
newReceiver.Spec.Title = fmt.Sprintf("receiver-2-%s", tc.user.Identity.GetLogin())
|
||||
if tc.canCreate {
|
||||
t.Run("should be able to create receiver", func(t *testing.T) {
|
||||
actual, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
|
||||
actual, err := client.Create(ctx, newReceiver, resource.CreateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
|
||||
require.Equal(t, newReceiver.Spec, actual.Spec)
|
||||
|
||||
t.Run("should fail if already exists", func(t *testing.T) {
|
||||
_, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, newReceiver, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "expected bad request but got %s", err)
|
||||
})
|
||||
|
||||
// Cleanup.
|
||||
require.NoError(t, adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{}))
|
||||
require.NoError(t, adminClient.Delete(ctx, actual.GetStaticMetadata().Identifier(), resource.DeleteOptions{}))
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to create", func(t *testing.T) {
|
||||
_, err := client.Create(ctx, newReceiver, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, newReceiver, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d))
|
||||
})
|
||||
}
|
||||
|
||||
// create resource to proceed with other tests. We don't use the one created above because the user will always
|
||||
// have admin permissions on it.
|
||||
expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{})
|
||||
expected, err = adminClient.Create(ctx, expected, resource.CreateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
require.NotNil(t, expected)
|
||||
|
||||
@@ -627,34 +633,34 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
expectedWithMetadata.SetAccessControl("canAdmin")
|
||||
}
|
||||
t.Run("should be able to list receivers", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 2) // default + created
|
||||
})
|
||||
|
||||
t.Run("should be able to read receiver by resource identifier", func(t *testing.T) {
|
||||
got, err := client.Get(ctx, expected.Name, v1.GetOptions{})
|
||||
got, err := client.Get(ctx, expected.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedWithMetadata, got)
|
||||
|
||||
t.Run("should get NotFound if resource does not exist", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
|
||||
_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("list receivers should be empty", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Emptyf(t, list.Items, "Expected no receivers but got %v", list.Items)
|
||||
})
|
||||
|
||||
t.Run("should be forbidden to read receiver by name", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, expected.Name, v1.GetOptions{})
|
||||
_, err := client.Get(ctx, expected.GetStaticMetadata().Identifier())
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should get forbidden even if name does not exist", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
|
||||
_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
@@ -668,7 +674,7 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
|
||||
if tc.canUpdate {
|
||||
t.Run("should be able to update receiver", func(t *testing.T) {
|
||||
updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
|
||||
updated, err := client.Update(ctx, updatedExpected, resource.UpdateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
|
||||
expected = updated
|
||||
@@ -676,7 +682,7 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
|
||||
up := updatedExpected.Copy().(*v0alpha1.Receiver)
|
||||
up.Name = "notFound"
|
||||
_, err := client.Update(ctx, up, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, up, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
@@ -686,7 +692,7 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
createIntegration(t, "webhook"),
|
||||
}
|
||||
|
||||
expected, err = adminClient.Update(ctx, updatedExpected, v1.UpdateOptions{})
|
||||
expected, err = adminClient.Update(ctx, updatedExpected, resource.UpdateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
require.NotNil(t, expected)
|
||||
|
||||
@@ -695,60 +701,62 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
|
||||
if tc.canUpdateProtected {
|
||||
t.Run("should be able to update protected fields of the receiver", func(t *testing.T) {
|
||||
updated, err := client.Update(ctx, updatedProtected, v1.UpdateOptions{})
|
||||
updated, err := client.Update(ctx, updatedProtected, resource.UpdateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
require.NotNil(t, updated)
|
||||
expected = updated
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to edit protected fields of the receiver", func(t *testing.T) {
|
||||
_, err := client.Update(ctx, updatedProtected, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, updatedProtected, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
t.Run("should be forbidden to update receiver", func(t *testing.T) {
|
||||
_, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, updatedExpected, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should get forbidden even if resource does not exist", func(t *testing.T) {
|
||||
up := updatedExpected.Copy().(*v0alpha1.Receiver)
|
||||
up.Name = "notFound"
|
||||
_, err := client.Update(ctx, up, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, up, resource.UpdateOptions{
|
||||
ResourceVersion: up.ResourceVersion,
|
||||
})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
require.Falsef(t, tc.canUpdateProtected, "Invalid combination of assertions. CanUpdateProtected should be false")
|
||||
}
|
||||
|
||||
deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}}
|
||||
deleteOptions := resource.DeleteOptions{Preconditions: resource.DeleteOptionsPreconditions{ResourceVersion: expected.ResourceVersion}}
|
||||
|
||||
if tc.canDelete {
|
||||
t.Run("should be able to delete receiver", func(t *testing.T) {
|
||||
err := client.Delete(ctx, expected.Name, deleteOptions)
|
||||
err := client.Delete(ctx, expected.GetStaticMetadata().Identifier(), deleteOptions)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
err := client.Delete(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "notfound"}, resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to delete receiver", func(t *testing.T) {
|
||||
err := client.Delete(ctx, expected.Name, deleteOptions)
|
||||
err := client.Delete(ctx, expected.GetStaticMetadata().Identifier(), deleteOptions)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should be forbidden even if resource does not exist", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
err := client.Delete(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "notfound"}, resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{}))
|
||||
require.NoError(t, adminClient.Delete(ctx, expected.GetStaticMetadata().Identifier(), resource.DeleteOptions{}))
|
||||
}
|
||||
|
||||
if tc.canRead {
|
||||
t.Run("should get empty list if no receivers", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
})
|
||||
@@ -766,7 +774,8 @@ func TestIntegrationInUseMetadata(t *testing.T) {
|
||||
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
|
||||
|
||||
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
// Prepare environment and create notification policy and rule that use receiver
|
||||
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
|
||||
require.NoError(t, err)
|
||||
@@ -813,7 +822,7 @@ func TestIntegrationInUseMetadata(t *testing.T) {
|
||||
|
||||
requestReceivers := func(t *testing.T, title string) (v0alpha1.Receiver, v0alpha1.Receiver) {
|
||||
t.Helper()
|
||||
receivers, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
receivers, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, receivers.Items, 2)
|
||||
idx := slices.IndexFunc(receivers.Items, func(interval v0alpha1.Receiver) bool {
|
||||
@@ -821,7 +830,7 @@ func TestIntegrationInUseMetadata(t *testing.T) {
|
||||
})
|
||||
receiverListed := receivers.Items[idx]
|
||||
|
||||
receiverGet, err := adminClient.Get(ctx, receiverListed.Name, v1.GetOptions{})
|
||||
receiverGet, err := adminClient.Get(ctx, receiverListed.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
return receiverListed, *receiverGet
|
||||
@@ -846,8 +855,9 @@ func TestIntegrationInUseMetadata(t *testing.T) {
|
||||
amConfig.AlertmanagerConfig.Route.Routes = amConfig.AlertmanagerConfig.Route.Routes[:1]
|
||||
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
|
||||
require.NoError(t, err)
|
||||
routeAdminClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
|
||||
_, err = routeAdminClient.Update(ctx, v1Route, v1.UpdateOptions{})
|
||||
routeAdminClient, err := v0alpha1.NewRoutingTreeClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
_, err = routeAdminClient.Update(ctx, v1Route, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
receiverListed, receiverGet = requestReceivers(t, "user-defined")
|
||||
@@ -868,7 +878,7 @@ func TestIntegrationInUseMetadata(t *testing.T) {
|
||||
amConfig.AlertmanagerConfig.Route.Routes = nil
|
||||
v1route, err := routingtree.ConvertToK8sResource(1, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
|
||||
require.NoError(t, err)
|
||||
_, err = routeAdminClient.Update(ctx, v1route, v1.UpdateOptions{})
|
||||
_, err = routeAdminClient.Update(ctx, v1route, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Remove the remaining rules.
|
||||
@@ -892,7 +902,8 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
org := helper.Org1
|
||||
|
||||
admin := org.Admin
|
||||
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
env := helper.GetEnv()
|
||||
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
|
||||
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
|
||||
@@ -908,7 +919,7 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
createIntegration(t, "email"),
|
||||
},
|
||||
},
|
||||
}, v1.CreateOptions{})
|
||||
}, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "none", created.GetProvenanceStatus())
|
||||
|
||||
@@ -917,23 +928,23 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
UID: *created.Spec.Integrations[0].Uid,
|
||||
}, admin.Identity.GetOrgID(), "API"))
|
||||
|
||||
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
got, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "API", got.GetProvenanceStatus())
|
||||
})
|
||||
|
||||
t.Run("should not let update if provisioned", func(t *testing.T) {
|
||||
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
got, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
updated := got.Copy().(*v0alpha1.Receiver)
|
||||
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "email"))
|
||||
|
||||
_, err = adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err = adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should not let delete if provisioned", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
|
||||
err := adminClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
}
|
||||
@@ -944,7 +955,10 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
oldClient := test_common.NewReceiverClient(t, helper.Org1.Admin) // TODO replace with regular client once Delete works
|
||||
|
||||
receiver := v0alpha1.Receiver{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
@@ -955,21 +969,22 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
|
||||
created, err := adminClient.Create(ctx, &receiver, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
require.NotEmpty(t, created.ResourceVersion)
|
||||
|
||||
t.Run("should forbid if version does not match", func(t *testing.T) {
|
||||
t.Run("should conflict if version does not match", func(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.Receiver)
|
||||
updated.ResourceVersion = "test"
|
||||
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err := adminClient.Update(ctx, updated, resource.UpdateOptions{
|
||||
ResourceVersion: "test",
|
||||
})
|
||||
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
t.Run("should update if version matches", func(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.Receiver)
|
||||
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "email"))
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
for i, integration := range actualUpdated.Spec.Integrations {
|
||||
updated.Spec.Integrations[i].Uid = integration.Uid
|
||||
@@ -981,25 +996,25 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.Receiver)
|
||||
updated.ResourceVersion = ""
|
||||
updated.Spec.Integrations = append(updated.Spec.Integrations, createIntegration(t, "webhook"))
|
||||
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err := oldClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err) // TODO Change that? K8s returns 400 instead.
|
||||
})
|
||||
t.Run("should fail to delete if version does not match", func(t *testing.T) {
|
||||
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
actual, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer("something"),
|
||||
},
|
||||
})
|
||||
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
|
||||
require.Truef(t, errors.IsConflict(err), "should get conflict error but got %s", err)
|
||||
})
|
||||
t.Run("should succeed if version matches", func(t *testing.T) {
|
||||
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
actual, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer(actual.ResourceVersion),
|
||||
},
|
||||
@@ -1007,10 +1022,10 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("should succeed if version is empty", func(t *testing.T) {
|
||||
actual, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
|
||||
actual, err := adminClient.Create(ctx, &receiver, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer(actual.ResourceVersion),
|
||||
},
|
||||
@@ -1025,7 +1040,8 @@ func TestIntegrationPatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
receiver := v0alpha1.Receiver{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
@@ -1040,40 +1056,40 @@ func TestIntegrationPatch(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
current, err := adminClient.Create(ctx, &receiver, v1.CreateOptions{})
|
||||
current, err := adminClient.Create(ctx, &receiver, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, current)
|
||||
|
||||
t.Run("should patch with json patch", func(t *testing.T) {
|
||||
current, err := adminClient.Get(ctx, current.Name, v1.GetOptions{})
|
||||
current, err := adminClient.Get(ctx, current.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
index := slices.IndexFunc(current.Spec.Integrations, func(t v0alpha1.ReceiverIntegration) bool {
|
||||
return t.Type == "webhook"
|
||||
})
|
||||
|
||||
patch := []map[string]any{
|
||||
patch := []resource.PatchOperation{
|
||||
{
|
||||
"op": "remove",
|
||||
"path": fmt.Sprintf("/spec/integrations/%d/settings/username", index),
|
||||
Operation: "remove",
|
||||
Path: fmt.Sprintf("/spec/integrations/%d/settings/username", index),
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": fmt.Sprintf("/spec/integrations/%d/secureFields/password", index),
|
||||
Operation: "remove",
|
||||
Path: fmt.Sprintf("/spec/integrations/%d/secureFields/password", index),
|
||||
},
|
||||
{
|
||||
"op": "replace",
|
||||
"path": fmt.Sprintf("/spec/integrations/%d/settings/authorization_scheme", index),
|
||||
"value": "bearer",
|
||||
Operation: "replace",
|
||||
Path: fmt.Sprintf("/spec/integrations/%d/settings/authorization_scheme", index),
|
||||
Value: "bearer",
|
||||
},
|
||||
{
|
||||
"op": "add",
|
||||
"path": fmt.Sprintf("/spec/integrations/%d/settings/authorization_credentials", index),
|
||||
"value": "authz-token",
|
||||
Operation: "add",
|
||||
Path: fmt.Sprintf("/spec/integrations/%d/settings/authorization_credentials", index),
|
||||
Value: "authz-token",
|
||||
},
|
||||
{
|
||||
"op": "remove",
|
||||
"path": fmt.Sprintf("/spec/integrations/%d/secureFields/authorization_credentials", index),
|
||||
Operation: "remove",
|
||||
Path: fmt.Sprintf("/spec/integrations/%d/secureFields/authorization_credentials", index),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1084,10 +1100,7 @@ func TestIntegrationPatch(t *testing.T) {
|
||||
delete(expected.SecureFields, "password")
|
||||
expected.SecureFields["authorization_credentials"] = true
|
||||
|
||||
patchData, err := json.Marshal(patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
|
||||
result, err := adminClient.Patch(ctx, current.GetStaticMetadata().Identifier(), resource.PatchRequest{Operations: patch}, resource.PatchOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EqualValues(t, expected, result.Spec.Integrations[index])
|
||||
@@ -1127,7 +1140,8 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
|
||||
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
|
||||
|
||||
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
// Prepare environment and create notification policy and rule that use time receiver
|
||||
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
|
||||
require.NoError(t, err)
|
||||
@@ -1146,7 +1160,7 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
|
||||
_, status, data := legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
|
||||
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
|
||||
|
||||
receivers, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
receivers, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, receivers.Items, 2)
|
||||
idx := slices.IndexFunc(receivers.Items, func(interval v0alpha1.Receiver) bool {
|
||||
@@ -1164,7 +1178,7 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
|
||||
expectedTitle := renamed.Spec.Title + "-new"
|
||||
renamed.Spec.Title = expectedTitle
|
||||
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
actual, err := adminClient.Update(ctx, renamed, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
|
||||
@@ -1178,7 +1192,7 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
|
||||
assert.Equalf(t, expectedTitle, route.Receiver, "time receiver in routes should have been renamed but it did not")
|
||||
}
|
||||
|
||||
actual, err = adminClient.Get(ctx, actual.Name, v1.GetOptions{})
|
||||
actual, err = adminClient.Get(ctx, actual.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
receiver = *actual
|
||||
@@ -1194,20 +1208,20 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, db.DeleteProvenance(ctx, ¤tRoute, orgID))
|
||||
})
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
actual, err := adminClient.Update(ctx, renamed, resource.UpdateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
t.Run("provisioned rules", func(t *testing.T) {
|
||||
ruleUid := currentRuleGroup.Rules[0].GrafanaManagedAlert.UID
|
||||
resource := &ngmodels.AlertRule{UID: ruleUid}
|
||||
require.NoError(t, db.SetProvenance(ctx, resource, orgID, "API"))
|
||||
rule := &ngmodels.AlertRule{UID: ruleUid}
|
||||
require.NoError(t, db.SetProvenance(ctx, rule, orgID, "API"))
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, db.DeleteProvenance(ctx, resource, orgID))
|
||||
require.NoError(t, db.DeleteProvenance(ctx, rule, orgID))
|
||||
})
|
||||
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
actual, err := adminClient.Update(ctx, renamed, resource.UpdateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
@@ -1216,7 +1230,7 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("should fail to delete if receiver is used in rule and routes", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, receiver.Name, v1.DeleteOptions{})
|
||||
err := adminClient.Delete(ctx, receiver.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
@@ -1225,7 +1239,7 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
|
||||
route.Routes[0].Receiver = ""
|
||||
legacyCli.UpdateRoute(t, route, true)
|
||||
|
||||
err = adminClient.Delete(ctx, receiver.Name, v1.DeleteOptions{})
|
||||
err = adminClient.Delete(ctx, receiver.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
})
|
||||
@@ -1237,10 +1251,11 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
var defaultReceiver *v0alpha1.Receiver
|
||||
t.Run("should list the default receiver", func(t *testing.T) {
|
||||
items, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
items, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, items.Items, 1)
|
||||
defaultReceiver = &items.Items[0]
|
||||
@@ -1249,7 +1264,7 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
assert.NotEmpty(t, defaultReceiver.Name)
|
||||
assert.NotEmpty(t, defaultReceiver.ResourceVersion)
|
||||
|
||||
defaultReceiver, err = adminClient.Get(ctx, defaultReceiver.Name, v1.GetOptions{})
|
||||
defaultReceiver, err = adminClient.Get(ctx, defaultReceiver.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, defaultReceiver.UID)
|
||||
assert.NotEmpty(t, defaultReceiver.Name)
|
||||
@@ -1262,7 +1277,7 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
newDefault := defaultReceiver.Copy().(*v0alpha1.Receiver)
|
||||
newDefault.Spec.Integrations = append(newDefault.Spec.Integrations, createIntegration(t, line.Type))
|
||||
|
||||
updatedReceiver, err := adminClient.Update(ctx, newDefault, v1.UpdateOptions{})
|
||||
updatedReceiver, err := adminClient.Update(ctx, newDefault, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := newDefault.Copy().(*v0alpha1.Receiver)
|
||||
@@ -1290,12 +1305,12 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
Integrations: []v0alpha1.ReceiverIntegration{},
|
||||
},
|
||||
}
|
||||
_, err := adminClient.Create(ctx, newReceiver, v1.CreateOptions{})
|
||||
_, err := adminClient.Create(ctx, newReceiver, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
t.Run("should not let delete default receiver", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, defaultReceiver.Name, v1.DeleteOptions{})
|
||||
err := adminClient.Delete(ctx, defaultReceiver.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
@@ -1317,7 +1332,7 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
Title: "all-receivers",
|
||||
Integrations: integrations,
|
||||
},
|
||||
}, v1.CreateOptions{})
|
||||
}, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, receiver.Spec.Integrations, len(integrations))
|
||||
|
||||
@@ -1342,7 +1357,7 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should be able read what it is created", func(t *testing.T) {
|
||||
get, err := adminClient.Get(ctx, receiver.Name, v1.GetOptions{})
|
||||
get, err := adminClient.Get(ctx, receiver.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, receiver, get)
|
||||
t.Run("should return secrets in secureFields but not settings", func(t *testing.T) {
|
||||
@@ -1394,7 +1409,7 @@ func TestIntegrationCRUD(t *testing.T) {
|
||||
Title: fmt.Sprintf("invalid-%s", key),
|
||||
Integrations: []v0alpha1.ReceiverIntegration{integration},
|
||||
},
|
||||
}, v1.CreateOptions{})
|
||||
}, resource.CreateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result: %v", receiver)
|
||||
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest, got: %s", err)
|
||||
})
|
||||
@@ -1408,7 +1423,8 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
recv1 := &v0alpha1.Receiver{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
@@ -1420,7 +1436,7 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
recv1, err := adminClient.Create(ctx, recv1, v1.CreateOptions{})
|
||||
recv1, err = adminClient.Create(ctx, recv1, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
recv2 := &v0alpha1.Receiver{
|
||||
@@ -1434,7 +1450,7 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
recv2, err = adminClient.Create(ctx, recv2, v1.CreateOptions{})
|
||||
recv2, err = adminClient.Create(ctx, recv2, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
env := helper.GetEnv()
|
||||
@@ -1444,18 +1460,20 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
|
||||
require.NoError(t, db.SetProvenance(ctx, &definitions.EmbeddedContactPoint{
|
||||
UID: *recv2.Spec.Integrations[0].Uid,
|
||||
}, helper.Org1.Admin.Identity.GetOrgID(), "API"))
|
||||
recv2, err = adminClient.Get(ctx, recv2.Name, v1.GetOptions{})
|
||||
recv2, err = adminClient.Get(ctx, recv2.GetStaticMetadata().Identifier())
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
receivers, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
receivers, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, receivers.Items, 3) // Includes default.
|
||||
|
||||
t.Run("should filter by receiver name", func(t *testing.T) {
|
||||
t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "spec.title=" + recv1.Spec.Title,
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{
|
||||
"spec.title=" + recv1.Spec.Title,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -1463,8 +1481,10 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should filter by metadata name", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "metadata.name=" + recv2.Name,
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{
|
||||
"metadata.name=" + recv2.Name,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -1473,8 +1493,10 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
|
||||
|
||||
t.Run("should filter by multiple filters", func(t *testing.T) {
|
||||
t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s,spec.title=%s", recv2.Name, recv2.Spec.Title),
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{
|
||||
fmt.Sprintf("metadata.name=%s,spec.title=%s", recv2.Name, recv2.Spec.Title),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -1482,8 +1504,10 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should be empty when filter does not match", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s", "unknown"),
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{
|
||||
fmt.Sprintf("metadata.name=%s", "unknown"),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, list.Items)
|
||||
@@ -1497,7 +1521,8 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig)
|
||||
|
||||
helper := getTestHelper(t)
|
||||
|
||||
receiverClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
|
||||
receiverClient, err := v0alpha1.NewReceiverClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
for _, receiver := range amConfig.AlertmanagerConfig.Receivers {
|
||||
if receiver.Name == "grafana-default-email" {
|
||||
continue
|
||||
@@ -1523,7 +1548,7 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig)
|
||||
})
|
||||
}
|
||||
|
||||
created, err := receiverClient.Create(ctx, &toCreate, v1.CreateOptions{})
|
||||
created, err := receiverClient.Create(ctx, &toCreate, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
for i, integration := range created.Spec.Integrations {
|
||||
@@ -1533,10 +1558,11 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig)
|
||||
|
||||
nsMapper := func(_ int64) string { return "default" }
|
||||
|
||||
routeClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
|
||||
routeClient, err := v0alpha1.NewRoutingTreeClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", nsMapper)
|
||||
require.NoError(t, err)
|
||||
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
|
||||
_, err = routeClient.Update(ctx, v1route, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
{
|
||||
"kind": "ReceiverList",
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"metadata": {},
|
||||
"items": [
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "Z3JhZmFuYS1kZWZhdWx0LWVtYWls",
|
||||
"namespace": "default",
|
||||
"uid": "zyXFk301pvwNz4HRPrTMKPMFO2934cPB7H1ZXmyM1TUX",
|
||||
"resourceVersion": "a82b34036bdabbc4",
|
||||
"annotations": {
|
||||
"grafana.com/access/canAdmin": "true",
|
||||
"grafana.com/access/canDelete": "true",
|
||||
@@ -15,53 +19,29 @@
|
||||
"grafana.com/inUse/routes": "1",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "none"
|
||||
},
|
||||
"name": "Z3JhZmFuYS1kZWZhdWx0LWVtYWls",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "a82b34036bdabbc4",
|
||||
"uid": "zyXFk301pvwNz4HRPrTMKPMFO2934cPB7H1ZXmyM1TUX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "grafana-default-email",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "email",
|
||||
"version": "v1",
|
||||
"disableResolveMessage": false,
|
||||
"settings": {
|
||||
"addresses": "\u003cexample@email.com\u003e"
|
||||
},
|
||||
"type": "email",
|
||||
"uid": "",
|
||||
"version": "v1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "grafana-default-email"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
"grafana.com/canUse": "false",
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "Z3JhZmFuYS1kZWZhdWx0LWVtYWlsdGVzdC1jcmVhdGUtZ2V0LWNvbmZpZw",
|
||||
"namespace": "default",
|
||||
"uid": "JzW6DIlcxj4sRN8A2ULcwTXAmm0Vs0Z68aEBqXSvxK0X",
|
||||
"resourceVersion": "b2823b50ffa1eff6",
|
||||
"uid": "JzW6DIlcxj4sRN8A2ULcwTXAmm0Vs0Z68aEBqXSvxK0X"
|
||||
},
|
||||
"spec": {
|
||||
"integrations": [],
|
||||
"title": "grafana-default-emailtest-create-get-config"
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -69,19 +49,36 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "ZGlzY29yZA",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "06e437697f62ac59",
|
||||
"uid": "8cH8Ql2S6VhPEVUhwlQEKYWyPbRJS7YKj2lEXdrehH8X"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "grafana-default-emailtest-create-get-config",
|
||||
"integrations": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "ZGlzY29yZA",
|
||||
"namespace": "default",
|
||||
"uid": "8cH8Ql2S6VhPEVUhwlQEKYWyPbRJS7YKj2lEXdrehH8X",
|
||||
"resourceVersion": "06e437697f62ac59",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
"grafana.com/canUse": "false",
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "discord",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "discord",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"webhook_url": true
|
||||
},
|
||||
"settings": {
|
||||
"http_config": {
|
||||
"enable_http2": true,
|
||||
@@ -95,18 +92,19 @@
|
||||
"send_resolved": true,
|
||||
"title": "{{ template \"discord.default.title\" . }}"
|
||||
},
|
||||
"type": "discord",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"webhook_url": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "discord"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "ZW1haWw",
|
||||
"namespace": "default",
|
||||
"uid": "bhlvlN758xmnwVrHVPX0c5XvFHepenUbOXP0fuE6eUMX",
|
||||
"resourceVersion": "9b3ffed277cee189",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -114,19 +112,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "ZW1haWw",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "9b3ffed277cee189",
|
||||
"uid": "bhlvlN758xmnwVrHVPX0c5XvFHepenUbOXP0fuE6eUMX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "email",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "email",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"auth_password": true
|
||||
},
|
||||
"settings": {
|
||||
"auth_username": "alertmanager",
|
||||
"from": "alertmanager@example.com",
|
||||
@@ -144,18 +139,19 @@
|
||||
},
|
||||
"to": "team@example.com"
|
||||
},
|
||||
"type": "email",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"auth_password": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "email"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "amlyYQ",
|
||||
"namespace": "default",
|
||||
"uid": "7Pu4xcRXbvw4XEX279SoqyO8Ibo8cMl0vAJyYTsJ0NEX",
|
||||
"resourceVersion": "deae9d34f8554205",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -163,19 +159,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "amlyYQ",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "deae9d34f8554205",
|
||||
"uid": "7Pu4xcRXbvw4XEX279SoqyO8Ibo8cMl0vAJyYTsJ0NEX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "jira",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "jira",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"http_config.basic_auth.password": true
|
||||
},
|
||||
"settings": {
|
||||
"api_url": "http://localhost/jira",
|
||||
"custom_fields": {
|
||||
@@ -203,18 +196,19 @@
|
||||
"send_resolved": true,
|
||||
"summary": "{{ template \"jira.default.summary\" . }}"
|
||||
},
|
||||
"type": "jira",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"http_config.basic_auth.password": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "jira"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "bXN0ZWFtcw",
|
||||
"namespace": "default",
|
||||
"uid": "z7xTMDjrk1HAHXPEx78tQb63LXYA6ivXLOtz2Z09ucIX",
|
||||
"resourceVersion": "95c8d082d65466a3",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -222,19 +216,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "bXN0ZWFtcw",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "95c8d082d65466a3",
|
||||
"uid": "z7xTMDjrk1HAHXPEx78tQb63LXYA6ivXLOtz2Z09ucIX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "msteams",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "teams",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"webhook_url": true
|
||||
},
|
||||
"settings": {
|
||||
"http_config": {
|
||||
"enable_http2": true,
|
||||
@@ -249,18 +240,19 @@
|
||||
"text": "{{ template \"msteams.default.text\" . }}",
|
||||
"title": "{{ template \"msteams.default.title\" . }}"
|
||||
},
|
||||
"type": "teams",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"webhook_url": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "msteams"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "b3BzZ2VuaWU",
|
||||
"namespace": "default",
|
||||
"uid": "XmkZ214Dj030hvynYiwNLq8i6uRCjUYXMXjE5m19OKAX",
|
||||
"resourceVersion": "8ee2957ba150ba16",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -268,19 +260,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "b3BzZ2VuaWU",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "8ee2957ba150ba16",
|
||||
"uid": "XmkZ214Dj030hvynYiwNLq8i6uRCjUYXMXjE5m19OKAX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "opsgenie",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "opsgenie",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"api_key": true
|
||||
},
|
||||
"settings": {
|
||||
"actions": "test actions",
|
||||
"api_url": "http://localhost/opsgenie/",
|
||||
@@ -311,18 +300,19 @@
|
||||
"tags": "test-tags",
|
||||
"update_alerts": true
|
||||
},
|
||||
"type": "opsgenie",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"api_key": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "opsgenie"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "cGFnZXJkdXR5",
|
||||
"namespace": "default",
|
||||
"uid": "QNitkUCkwzrIc7WVCCJGGDyvXLyo9csSUVqfyStyctQX",
|
||||
"resourceVersion": "fe673d5dcd67ccf0",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -330,20 +320,16 @@
|
||||
"grafana.com/inUse/routes": "1",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "cGFnZXJkdXR5",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "fe673d5dcd67ccf0",
|
||||
"uid": "QNitkUCkwzrIc7WVCCJGGDyvXLyo9csSUVqfyStyctQX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "pagerduty",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "pagerduty",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"routing_key": true,
|
||||
"service_key": true
|
||||
},
|
||||
"settings": {
|
||||
"class": "test class",
|
||||
"client": "Alertmanager",
|
||||
@@ -383,18 +369,20 @@
|
||||
"source": "test source",
|
||||
"url": "http://localhost/pagerduty"
|
||||
},
|
||||
"type": "pagerduty",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"routing_key": true,
|
||||
"service_key": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "pagerduty"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "cHVzaG92ZXI",
|
||||
"namespace": "default",
|
||||
"uid": "t2TJSktI6vyGfdbLOKmxH4eBqgcIGsAuW8Qm9m0HRycX",
|
||||
"resourceVersion": "6ae076725ab463e0",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -402,21 +390,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "cHVzaG92ZXI",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "6ae076725ab463e0",
|
||||
"uid": "t2TJSktI6vyGfdbLOKmxH4eBqgcIGsAuW8Qm9m0HRycX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "pushover",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "pushover",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"http_config.authorization.credentials": true,
|
||||
"token": true,
|
||||
"user_key": true
|
||||
},
|
||||
"settings": {
|
||||
"expire": "1h0m0s",
|
||||
"http_config": {
|
||||
@@ -437,18 +420,21 @@
|
||||
"title": "{{ template \"pushover.default.title\" . }}",
|
||||
"url": "http://localhost/pushover"
|
||||
},
|
||||
"type": "pushover",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"http_config.authorization.credentials": true,
|
||||
"token": true,
|
||||
"user_key": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "pushover"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "c2xhY2s",
|
||||
"namespace": "default",
|
||||
"uid": "xSB0hnoc9j1CnLCHR3VgeVGXdVXILM0p2dM64bbHN9oX",
|
||||
"resourceVersion": "ec0e343029ff5d8b",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -456,19 +442,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "c2xhY2s",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "ec0e343029ff5d8b",
|
||||
"uid": "xSB0hnoc9j1CnLCHR3VgeVGXdVXILM0p2dM64bbHN9oX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "slack",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "slack",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"api_url": true
|
||||
},
|
||||
"settings": {
|
||||
"actions": [
|
||||
{
|
||||
@@ -522,18 +505,19 @@
|
||||
"title_link": "http://localhost",
|
||||
"username": "Alerting Team"
|
||||
},
|
||||
"type": "slack",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"api_url": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "slack"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "c25z",
|
||||
"namespace": "default",
|
||||
"uid": "vSP8NtFr23hnqZqLxRgzUKfr1wOemOvZm1S6MYkfRI4X",
|
||||
"resourceVersion": "77d734ad4c196d36",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -541,19 +525,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "c25z",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "77d734ad4c196d36",
|
||||
"uid": "vSP8NtFr23hnqZqLxRgzUKfr1wOemOvZm1S6MYkfRI4X"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "sns",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "sns",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"sigv4.SecretKey": true
|
||||
},
|
||||
"settings": {
|
||||
"attributes": {
|
||||
"key1": "value1"
|
||||
@@ -577,18 +558,19 @@
|
||||
"subject": "{{ template \"sns.default.subject\" . }}",
|
||||
"topic_arn": "arn:aws:sns:us-east-1:123456789012:alerts"
|
||||
},
|
||||
"type": "sns",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"sigv4.SecretKey": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "sns"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "dGVsZWdyYW0",
|
||||
"namespace": "default",
|
||||
"uid": "XLWjtmYcjP5PiqBCwZXX3YKHV1G8niRtpCakIpcHqoYX",
|
||||
"resourceVersion": "d9850878a33e302e",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -596,19 +578,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "dGVsZWdyYW0",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "d9850878a33e302e",
|
||||
"uid": "XLWjtmYcjP5PiqBCwZXX3YKHV1G8niRtpCakIpcHqoYX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "telegram",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "telegram",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"token": true
|
||||
},
|
||||
"settings": {
|
||||
"api_url": "http://localhost/telegram-default",
|
||||
"chat": -1001234567890,
|
||||
@@ -624,18 +603,19 @@
|
||||
"parse_mode": "MarkdownV2",
|
||||
"send_resolved": true
|
||||
},
|
||||
"type": "telegram",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"token": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "telegram"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "dmljdG9yb3Bz",
|
||||
"namespace": "default",
|
||||
"uid": "EWiwQ6TIW0GpEo46WusW7Nvg0HuD4QAbHf0JZ2OSOhEX",
|
||||
"resourceVersion": "1e6886531440afc2",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -643,19 +623,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "dmljdG9yb3Bz",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "1e6886531440afc2",
|
||||
"uid": "EWiwQ6TIW0GpEo46WusW7Nvg0HuD4QAbHf0JZ2OSOhEX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "victorops",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "victorops",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"api_key": true
|
||||
},
|
||||
"settings": {
|
||||
"api_url": "http://localhost/victorops-default/",
|
||||
"entity_display_name": "{{ template \"victorops.default.entity_display_name\" . }}",
|
||||
@@ -674,18 +651,19 @@
|
||||
"send_resolved": true,
|
||||
"state_message": "{{ template \"victorops.default.state_message\" . }}"
|
||||
},
|
||||
"type": "victorops",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"api_key": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "victorops"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "d2ViZXg",
|
||||
"namespace": "default",
|
||||
"uid": "wDNufI44UXHWq4ERRYenZ7XgXVV3Tjxaokz9IjMRZ54X",
|
||||
"resourceVersion": "08fc955a08dfe9c0",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -693,19 +671,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "d2ViZXg",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "08fc955a08dfe9c0",
|
||||
"uid": "wDNufI44UXHWq4ERRYenZ7XgXVV3Tjxaokz9IjMRZ54X"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "webex",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "webex",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"http_config.authorization.credentials": true
|
||||
},
|
||||
"settings": {
|
||||
"api_url": "http://localhost/webes-default",
|
||||
"http_config": {
|
||||
@@ -723,18 +698,19 @@
|
||||
"room_id": "Y2lzY29zcGFyazovL3VzL1JPT00v12345678",
|
||||
"send_resolved": true
|
||||
},
|
||||
"type": "webex",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"http_config.authorization.credentials": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "webex"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "d2ViaG9vaw",
|
||||
"namespace": "default",
|
||||
"uid": "aKzigXATPp6HOh20yTrlTcuF2Y9IrPHridGIcWrJygsX",
|
||||
"resourceVersion": "494392f899a7b410",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -742,19 +718,16 @@
|
||||
"grafana.com/inUse/routes": "1",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "d2ViaG9vaw",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "494392f899a7b410",
|
||||
"uid": "aKzigXATPp6HOh20yTrlTcuF2Y9IrPHridGIcWrJygsX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "webhook",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "webhook",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"url": true
|
||||
},
|
||||
"settings": {
|
||||
"http_config": {
|
||||
"enable_http2": true,
|
||||
@@ -769,18 +742,19 @@
|
||||
"timeout": "0s",
|
||||
"url_file": ""
|
||||
},
|
||||
"type": "webhook",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"url": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "webhook"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "notifications.alerting.grafana.app/v0alpha1",
|
||||
"kind": "Receiver",
|
||||
"metadata": {
|
||||
"name": "d2VjaGF0",
|
||||
"namespace": "default",
|
||||
"uid": "jkXCvNrNVw7XX5nmYFyrGiA4ckAvJ282u2scW8KZq7IX",
|
||||
"resourceVersion": "135913515cbc156b",
|
||||
"annotations": {
|
||||
"grafana.com/access/canModifyProtected": "true",
|
||||
"grafana.com/access/canReadSecrets": "true",
|
||||
@@ -788,19 +762,16 @@
|
||||
"grafana.com/inUse/routes": "0",
|
||||
"grafana.com/inUse/rules": "0",
|
||||
"grafana.com/provenance": "converted_prometheus"
|
||||
},
|
||||
"name": "d2VjaGF0",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "135913515cbc156b",
|
||||
"uid": "jkXCvNrNVw7XX5nmYFyrGiA4ckAvJ282u2scW8KZq7IX"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "wechat",
|
||||
"integrations": [
|
||||
{
|
||||
"uid": "",
|
||||
"type": "wechat",
|
||||
"version": "v0mimir1",
|
||||
"disableResolveMessage": false,
|
||||
"secureFields": {
|
||||
"api_secret": true
|
||||
},
|
||||
"settings": {
|
||||
"agent_id": "1000002",
|
||||
"api_url": "http://localhost/wechat/",
|
||||
@@ -820,15 +791,12 @@
|
||||
"to_tag": "tag1",
|
||||
"to_user": "user1"
|
||||
},
|
||||
"type": "wechat",
|
||||
"uid": "",
|
||||
"version": "v0mimir1"
|
||||
"secureFields": {
|
||||
"api_secret": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "wechat"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"kind": "ReceiverList",
|
||||
"metadata": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
"github.com/prometheus/common/model"
|
||||
@@ -39,6 +40,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
|
||||
var defaultTreeIdentifier = resource.Identifier{
|
||||
Namespace: apis.DefaultNamespace,
|
||||
Name: v0alpha1.UserDefinedRoutingTreeName,
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
@@ -52,7 +58,8 @@ func TestIntegrationNotAllowedMethods(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
client := common.NewRoutingTreeClient(t, helper.Org1.Admin)
|
||||
client, err := v0alpha1.NewRoutingTreeClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
route := &v0alpha1.RoutingTree{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -60,11 +67,7 @@ func TestIntegrationNotAllowedMethods(t *testing.T) {
|
||||
},
|
||||
Spec: v0alpha1.RoutingTreeSpec{},
|
||||
}
|
||||
_, err := client.Create(ctx, route, v1.CreateOptions{})
|
||||
assert.Error(t, err)
|
||||
require.Truef(t, errors.IsMethodNotSupported(err), "Expected MethodNotSupported but got %s", err)
|
||||
|
||||
err = client.Client.DeleteCollection(ctx, v1.DeleteOptions{}, v1.ListOptions{})
|
||||
_, err = client.Create(ctx, route, resource.CreateOptions{})
|
||||
assert.Error(t, err)
|
||||
require.Truef(t, errors.IsMethodNotSupported(err), "Expected MethodNotSupported but got %s", err)
|
||||
}
|
||||
@@ -154,50 +157,52 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
}
|
||||
|
||||
admin := org1.Admin
|
||||
adminClient := common.NewRoutingTreeClient(t, admin)
|
||||
adminClient, err := v0alpha1.NewRoutingTreeClientFromGenerator(admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
|
||||
client := common.NewRoutingTreeClient(t, tc.user)
|
||||
client, err := v0alpha1.NewRoutingTreeClientFromGenerator(tc.user.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.canRead {
|
||||
t.Run("should be able to list routing trees", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
require.Equal(t, v0alpha1.UserDefinedRoutingTreeName, list.Items[0].Name)
|
||||
})
|
||||
|
||||
t.Run("should be able to read routing trees by resource identifier", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
_, err := client.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should get NotFound if resource does not exist", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
|
||||
_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to list routing trees", func(t *testing.T) {
|
||||
_, err := client.List(ctx, v1.ListOptions{})
|
||||
_, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should be forbidden to read routing tree by name", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
_, err := client.Get(ctx, defaultTreeIdentifier)
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should get forbidden even if name does not exist", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
|
||||
_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
current, err := adminClient.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
expected := current.Copy().(*v0alpha1.RoutingTree)
|
||||
expected.Spec.Routes = []v0alpha1.RoutingTreeRoute{
|
||||
@@ -217,7 +222,7 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
|
||||
if tc.canUpdate {
|
||||
t.Run("should be able to update routing tree", func(t *testing.T) {
|
||||
updated, err := client.Update(ctx, expected, v1.UpdateOptions{})
|
||||
updated, err := client.Update(ctx, expected, resource.UpdateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
|
||||
expected = updated
|
||||
@@ -225,21 +230,23 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
|
||||
up := expected.Copy().(*v0alpha1.RoutingTree)
|
||||
up.Name = "notFound"
|
||||
_, err := client.Update(ctx, up, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, up, resource.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to update routing tree", func(t *testing.T) {
|
||||
_, err := client.Update(ctx, expected, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, expected, resource.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should get forbidden even if resource does not exist", func(t *testing.T) {
|
||||
up := expected.Copy().(*v0alpha1.RoutingTree)
|
||||
up.Name = "notFound"
|
||||
_, err := client.Update(ctx, up, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, up, resource.UpdateOptions{
|
||||
ResourceVersion: up.ResourceVersion,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
@@ -248,32 +255,32 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
|
||||
if tc.canUpdate {
|
||||
t.Run("should be able to reset routing tree", func(t *testing.T) {
|
||||
err := client.Delete(ctx, expected.Name, v1.DeleteOptions{})
|
||||
err := client.Delete(ctx, expected.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
err := client.Delete(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "notfound"}, resource.DeleteOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to reset routing tree", func(t *testing.T) {
|
||||
err := client.Delete(ctx, expected.Name, v1.DeleteOptions{})
|
||||
err := client.Delete(ctx, expected.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should be forbidden even if resource does not exist", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
err := client.Delete(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "notfound"}, resource.DeleteOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{}))
|
||||
require.NoError(t, adminClient.Delete(ctx, expected.GetStaticMetadata().Identifier(), resource.DeleteOptions{}))
|
||||
}
|
||||
})
|
||||
|
||||
err := adminClient.Delete(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.DeleteOptions{})
|
||||
err := adminClient.Delete(ctx, defaultTreeIdentifier, resource.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
@@ -287,21 +294,22 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
org := helper.Org1
|
||||
|
||||
admin := org.Admin
|
||||
adminClient := common.NewRoutingTreeClient(t, admin)
|
||||
adminClient, err := v0alpha1.NewRoutingTreeClientFromGenerator(admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
env := helper.GetEnv()
|
||||
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
|
||||
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
|
||||
require.NoError(t, err)
|
||||
|
||||
current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
current, err := adminClient.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "none", current.GetProvenanceStatus())
|
||||
|
||||
t.Run("should provide provenance status", func(t *testing.T) {
|
||||
require.NoError(t, db.SetProvenance(ctx, &definitions.Route{}, admin.Identity.GetOrgID(), "API"))
|
||||
|
||||
got, err := adminClient.Get(ctx, current.Name, v1.GetOptions{})
|
||||
got, err := adminClient.Get(ctx, current.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "API", got.GetProvenanceStatus())
|
||||
})
|
||||
@@ -319,13 +327,13 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should not let delete if provisioned", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, current.Name, v1.DeleteOptions{})
|
||||
err := adminClient.Delete(ctx, current.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
}
|
||||
@@ -336,35 +344,37 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewRoutingTreeClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
current, err := adminClient.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, current.ResourceVersion)
|
||||
|
||||
t.Run("should forbid if version does not match", func(t *testing.T) {
|
||||
updated := current.Copy().(*v0alpha1.RoutingTree)
|
||||
updated.ResourceVersion = "test"
|
||||
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err := adminClient.Update(ctx, updated, resource.UpdateOptions{
|
||||
ResourceVersion: "test",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
t.Run("should update if version matches", func(t *testing.T) {
|
||||
updated := current.Copy().(*v0alpha1.RoutingTree)
|
||||
updated.Spec.Defaults.GroupBy = append(updated.Spec.Defaults.GroupBy, "data")
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
|
||||
require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion)
|
||||
})
|
||||
t.Run("should update if version is empty", func(t *testing.T) {
|
||||
current, err = adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
current, err = adminClient.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
updated := current.Copy().(*v0alpha1.RoutingTree)
|
||||
updated.ResourceVersion = ""
|
||||
updated.Spec.Routes = append(updated.Spec.Routes, v0alpha1.RoutingTreeRoute{Continue: true})
|
||||
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
|
||||
require.NotEqual(t, current.ResourceVersion, actualUpdated.ResourceVersion)
|
||||
@@ -380,20 +390,22 @@ func TestIntegrationDataConsistency(t *testing.T) {
|
||||
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
|
||||
|
||||
client := common.NewRoutingTreeClient(t, helper.Org1.Admin)
|
||||
client, err := v0alpha1.NewRoutingTreeClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
receiver := "grafana-default-email"
|
||||
timeInterval := "test-time-interval"
|
||||
createRoute := func(t *testing.T, route definitions.Route) {
|
||||
t.Helper()
|
||||
routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
|
||||
routeClient, err := v0alpha1.NewRoutingTreeClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), route, "", func(int64) string { return "default" })
|
||||
require.NoError(t, err)
|
||||
_, err = routeClient.Update(ctx, v1Route, v1.UpdateOptions{})
|
||||
_, err = routeClient.Update(ctx, v1Route, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err := common.NewTimeIntervalClient(t, helper.Org1.Admin).Create(ctx, &v0alpha1.TimeInterval{
|
||||
_, err = common.NewTimeIntervalClient(t, helper.Org1.Admin).Create(ctx, &v0alpha1.TimeInterval{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
},
|
||||
@@ -435,7 +447,7 @@ func TestIntegrationDataConsistency(t *testing.T) {
|
||||
},
|
||||
}
|
||||
createRoute(t, route)
|
||||
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
tree, err := client.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
expected := []v0alpha1.RoutingTreeMatcher{
|
||||
{
|
||||
@@ -503,9 +515,9 @@ func TestIntegrationDataConsistency(t *testing.T) {
|
||||
ensureMatcher(t, labels.MatchNotEqual, "matchers", "v"),
|
||||
}
|
||||
|
||||
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
tree, err := client.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
_, err = client.Update(ctx, tree, v1.UpdateOptions{})
|
||||
_, err = client.Update(ctx, tree, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, _, _ = legacyCli.GetAlertmanagerConfigWithStatus(t)
|
||||
@@ -542,7 +554,7 @@ func TestIntegrationDataConsistency(t *testing.T) {
|
||||
createRoute(t, route)
|
||||
|
||||
t.Run("correctly reads all fields", func(t *testing.T) {
|
||||
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
tree, err := client.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, v0alpha1.RoutingTreeRouteDefaults{
|
||||
Receiver: receiver,
|
||||
@@ -589,10 +601,10 @@ func TestIntegrationDataConsistency(t *testing.T) {
|
||||
t.Run("correctly save all fields", func(t *testing.T) {
|
||||
before, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t)
|
||||
require.Equalf(t, http.StatusOK, status, body)
|
||||
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
tree, err := client.Get(ctx, defaultTreeIdentifier)
|
||||
tree.Spec.Defaults.GroupBy = []string{"test-123", "test-456", "test-789"}
|
||||
require.NoError(t, err)
|
||||
_, err = client.Update(ctx, tree, v1.UpdateOptions{})
|
||||
_, err = client.Update(ctx, tree, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
before.AlertmanagerConfig.Route.GroupByStr = []string{"test-123", "test-456", "test-789"}
|
||||
@@ -640,7 +652,7 @@ func TestIntegrationDataConsistency(t *testing.T) {
|
||||
}
|
||||
|
||||
createRoute(t, route)
|
||||
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
tree, err := client.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "foo🙂", tree.Spec.Routes[0].GroupBy[0])
|
||||
expected := []v0alpha1.RoutingTreeMatcher{
|
||||
@@ -666,7 +678,8 @@ func TestIntegrationExtraConfigsConflicts(t *testing.T) {
|
||||
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
|
||||
|
||||
client := common.NewRoutingTreeClient(t, helper.Org1.Admin)
|
||||
client, err := v0alpha1.NewRoutingTreeClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now upload a new extra config
|
||||
testAlertmanagerConfigYAML := `
|
||||
@@ -691,7 +704,7 @@ receivers:
|
||||
}, headers)
|
||||
require.Equal(t, "success", response.Status)
|
||||
|
||||
current, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
|
||||
current, err := client.Get(ctx, defaultTreeIdentifier)
|
||||
require.NoError(t, err)
|
||||
updated := current.Copy().(*v0alpha1.RoutingTree)
|
||||
updated.Spec.Routes = append(updated.Spec.Routes, v0alpha1.RoutingTreeRoute{
|
||||
@@ -704,7 +717,7 @@ receivers:
|
||||
},
|
||||
})
|
||||
|
||||
_, err = client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err = client.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsBadRequest(err), "Should get BadRequest error but got: %s", err)
|
||||
|
||||
@@ -712,6 +725,6 @@ receivers:
|
||||
legacyCli.ConvertPrometheusDeleteAlertmanagerConfig(t, headers)
|
||||
|
||||
// and try again
|
||||
_, err = client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err = client.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v3"
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/tests/api/alerting"
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/util/testutil"
|
||||
)
|
||||
@@ -35,7 +35,8 @@ func TestIntegrationImportedTemplates(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
|
||||
client, err := v0alpha1.NewTemplateGroupClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
cliCfg := helper.Org1.Admin.NewRestConfig()
|
||||
alertingApi := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
|
||||
@@ -57,7 +58,7 @@ func TestIntegrationImportedTemplates(t *testing.T) {
|
||||
response := alertingApi.ConvertPrometheusPostAlertmanagerConfig(t, amConfig, headers)
|
||||
require.Equal(t, "success", response.Status)
|
||||
|
||||
templates, err := client.List(context.Background(), metav1.ListOptions{})
|
||||
templates, err := client.List(context.Background(), apis.DefaultNamespace, resource.ListOptions{})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates.Items, 3)
|
||||
@@ -90,12 +91,12 @@ func TestIntegrationImportedTemplates(t *testing.T) {
|
||||
t.Run("should not be able to update", func(t *testing.T) {
|
||||
tpl := templates.Items[1]
|
||||
tpl.Spec.Content = "new content"
|
||||
_, err := client.Update(context.Background(), &tpl, metav1.UpdateOptions{})
|
||||
_, err := client.Update(context.Background(), &tpl, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should not be able to delete", func(t *testing.T) {
|
||||
err := client.Delete(context.Background(), templates.Items[1].Name, metav1.DeleteOptions{})
|
||||
err := client.Delete(context.Background(), templates.Items[1].GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
|
||||
})
|
||||
|
||||
@@ -108,14 +109,14 @@ func TestIntegrationImportedTemplates(t *testing.T) {
|
||||
}
|
||||
tpl.Spec.Kind = v0alpha1.TemplateGroupTemplateKindGrafana
|
||||
|
||||
created, err := client.Create(context.Background(), &tpl, metav1.CreateOptions{})
|
||||
created, err := client.Create(context.Background(), &tpl, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEqual(t, templates.Items[1].Name, created.Name)
|
||||
})
|
||||
|
||||
t.Run("sort by kind and then name", func(t *testing.T) {
|
||||
templates, err := client.List(context.Background(), metav1.ListOptions{})
|
||||
templates, err := client.List(context.Background(), apis.DefaultNamespace, resource.ListOptions{})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates.Items, 4)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/alerting/templates"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -45,7 +46,8 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
|
||||
client, err := v0alpha1.NewTemplateGroupClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
newTemplate := &v0alpha1.TemplateGroup{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -61,23 +63,23 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
t.Run("create should fail if object name is specified", func(t *testing.T) {
|
||||
template := newTemplate.Copy().(*v0alpha1.TemplateGroup)
|
||||
template.Name = "new-templateGroup"
|
||||
_, err := client.Create(ctx, template, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, template, resource.CreateOptions{})
|
||||
assert.Error(t, err)
|
||||
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
|
||||
})
|
||||
|
||||
var resourceID string
|
||||
var resourceID resource.Identifier
|
||||
t.Run("create should succeed and provide resource name", func(t *testing.T) {
|
||||
actual, err := client.Create(ctx, newTemplate, v1.CreateOptions{})
|
||||
actual, err := client.Create(ctx, newTemplate, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
require.NotEmptyf(t, actual.UID, "Resource UID should not be empty")
|
||||
resourceID = actual.Name
|
||||
resourceID = actual.GetStaticMetadata().Identifier()
|
||||
})
|
||||
|
||||
var existingTemplateGroup *v0alpha1.TemplateGroup
|
||||
t.Run("resource should be available by the identifier", func(t *testing.T) {
|
||||
actual, err := client.Get(ctx, resourceID, v1.GetOptions{})
|
||||
actual, err := client.Get(ctx, resourceID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
require.Equal(t, newTemplate.Spec, actual.Spec)
|
||||
@@ -90,12 +92,12 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
}
|
||||
updated := existingTemplateGroup.Copy().(*v0alpha1.TemplateGroup)
|
||||
updated.Spec.Title = "another-templateGroup"
|
||||
actual, err := client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actual, err := client.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, updated.Spec, actual.Spec)
|
||||
require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
|
||||
|
||||
resource, err := client.Get(ctx, actual.Name, v1.GetOptions{})
|
||||
resource, err := client.Get(ctx, actual.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, actual, resource)
|
||||
|
||||
@@ -104,7 +106,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
|
||||
var defaultTemplateGroup *v0alpha1.TemplateGroup
|
||||
t.Run("default template should be available by the identifier", func(t *testing.T) {
|
||||
actual, err := client.Get(ctx, templates.DefaultTemplateName, v1.GetOptions{})
|
||||
actual, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: templates.DefaultTemplateName})
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
|
||||
@@ -122,7 +124,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
t.Run("create with reserved default title should work", func(t *testing.T) {
|
||||
template := newTemplate.Copy().(*v0alpha1.TemplateGroup)
|
||||
template.Spec.Title = defaultTemplateGroup.Spec.Title
|
||||
actual, err := client.Create(ctx, template, v1.CreateOptions{})
|
||||
actual, err := client.Create(ctx, template, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
require.NotEmptyf(t, actual.UID, "Resource UID should not be empty")
|
||||
@@ -130,7 +132,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("default template should not be available by calculated UID", func(t *testing.T) {
|
||||
actual, err := client.Get(ctx, newTemplateWithOverlappingName.Name, v1.GetOptions{})
|
||||
actual, err := client.Get(ctx, newTemplateWithOverlappingName.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
|
||||
@@ -215,11 +217,13 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
adminClient := common.NewTemplateGroupClient(t, org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTemplateGroupClientFromGenerator(org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
|
||||
client := common.NewTemplateGroupClient(t, tc.user)
|
||||
client, err := v0alpha1.NewTemplateGroupClientFromGenerator(tc.user.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
var expected = &v0alpha1.TemplateGroup{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -237,12 +241,12 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
|
||||
if tc.canCreate {
|
||||
t.Run("should be able to create template group", func(t *testing.T) {
|
||||
actual, err := client.Create(ctx, expected, v1.CreateOptions{})
|
||||
actual, err := client.Create(ctx, expected, resource.CreateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
require.Equal(t, expected.Spec, actual.Spec)
|
||||
|
||||
t.Run("should fail if already exists", func(t *testing.T) {
|
||||
_, err := client.Create(ctx, actual, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, actual, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
|
||||
})
|
||||
|
||||
@@ -250,45 +254,45 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to create", func(t *testing.T) {
|
||||
_, err := client.Create(ctx, expected, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, expected, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d))
|
||||
})
|
||||
|
||||
// create resource to proceed with other tests
|
||||
expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{})
|
||||
expected, err = adminClient.Create(ctx, expected, resource.CreateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
require.NotNil(t, expected)
|
||||
}
|
||||
|
||||
if tc.canRead {
|
||||
t.Run("should be able to list template groups", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 2) // Includes default template.
|
||||
})
|
||||
|
||||
t.Run("should be able to read template group by resource identifier", func(t *testing.T) {
|
||||
got, err := client.Get(ctx, expected.Name, v1.GetOptions{})
|
||||
got, err := client.Get(ctx, expected.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, got)
|
||||
require.Equal(t, expected.Spec, got.Spec)
|
||||
|
||||
t.Run("should get NotFound if resource does not exist", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
|
||||
_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to list template groups", func(t *testing.T) {
|
||||
_, err := client.List(ctx, v1.ListOptions{})
|
||||
_, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should be forbidden to read template group by name", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, expected.Name, v1.GetOptions{})
|
||||
_, err := client.Get(ctx, expected.GetStaticMetadata().Identifier())
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should get forbidden even if name does not exist", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
|
||||
_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
@@ -302,7 +306,7 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
|
||||
if tc.canUpdate {
|
||||
t.Run("should be able to update template group", func(t *testing.T) {
|
||||
updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
|
||||
updated, err := client.Update(ctx, updatedExpected, resource.UpdateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
|
||||
expected = updated
|
||||
@@ -310,52 +314,54 @@ func TestIntegrationAccessControl(t *testing.T) {
|
||||
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
|
||||
up := updatedExpected.Copy().(*v0alpha1.TemplateGroup)
|
||||
up.Name = "notFound"
|
||||
_, err := client.Update(ctx, up, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, up, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to update template group", func(t *testing.T) {
|
||||
_, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, updatedExpected, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should get forbidden even if resource does not exist", func(t *testing.T) {
|
||||
up := updatedExpected.Copy().(*v0alpha1.TemplateGroup)
|
||||
up.Name = "notFound"
|
||||
_, err := client.Update(ctx, up, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, up, resource.UpdateOptions{
|
||||
ResourceVersion: up.ResourceVersion,
|
||||
})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}}
|
||||
|
||||
oldClient := common.NewTemplateGroupClient(t, tc.user) // TODO replace with normal client once delete is fixed
|
||||
if tc.canDelete {
|
||||
t.Run("should be able to delete template group", func(t *testing.T) {
|
||||
err := client.Delete(ctx, expected.Name, deleteOptions)
|
||||
err := oldClient.Delete(ctx, expected.GetStaticMetadata().Identifier().Name, deleteOptions)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
err := oldClient.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to delete template group", func(t *testing.T) {
|
||||
err := client.Delete(ctx, expected.Name, deleteOptions)
|
||||
err := oldClient.Delete(ctx, expected.GetStaticMetadata().Identifier().Name, deleteOptions)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should be forbidden even if resource does not exist", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
err := oldClient.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{}))
|
||||
require.NoError(t, adminClient.Delete(ctx, expected.GetStaticMetadata().Identifier(), resource.DeleteOptions{}))
|
||||
}
|
||||
|
||||
if tc.canRead {
|
||||
t.Run("should get list with just default template if no template groups", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
require.Equal(t, templates.DefaultTemplateName, list.Items[0].Name)
|
||||
@@ -374,7 +380,8 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
org := helper.Org1
|
||||
|
||||
admin := org.Admin
|
||||
adminClient := common.NewTemplateGroupClient(t, admin)
|
||||
adminClient, err := v0alpha1.NewTemplateGroupClientFromGenerator(admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
env := helper.GetEnv()
|
||||
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
|
||||
@@ -390,7 +397,7 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
Content: `{{ define "test" }} test {{ end }}`,
|
||||
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
|
||||
},
|
||||
}, v1.CreateOptions{})
|
||||
}, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "none", created.GetProvenanceStatus())
|
||||
|
||||
@@ -399,7 +406,7 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
Name: created.Spec.Title,
|
||||
}, admin.Identity.GetOrgID(), "API"))
|
||||
|
||||
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
got, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "API", got.GetProvenanceStatus())
|
||||
})
|
||||
@@ -407,12 +414,12 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.TemplateGroup)
|
||||
updated.Spec.Content = `{{ define "another-test" }} test {{ end }}`
|
||||
|
||||
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should not let delete if provisioned", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
|
||||
err := adminClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
}
|
||||
@@ -423,8 +430,9 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
|
||||
|
||||
adminClient, err := v0alpha1.NewTemplateGroupClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
oldClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
|
||||
template := v0alpha1.TemplateGroup{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
@@ -436,21 +444,22 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, &template, v1.CreateOptions{})
|
||||
created, err := adminClient.Create(ctx, &template, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
require.NotEmpty(t, created.ResourceVersion)
|
||||
|
||||
t.Run("should forbid if version does not match", func(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.TemplateGroup)
|
||||
updated.ResourceVersion = "test"
|
||||
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err := adminClient.Update(ctx, updated, resource.UpdateOptions{
|
||||
ResourceVersion: "test",
|
||||
})
|
||||
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
t.Run("should update if version matches", func(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.TemplateGroup)
|
||||
updated.Spec.Content = `{{ define "test-another" }} test {{ end }}`
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
|
||||
require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion)
|
||||
@@ -460,16 +469,16 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
updated.ResourceVersion = ""
|
||||
updated.Spec.Content = `{{ define "test-another-2" }} test {{ end }}`
|
||||
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
|
||||
require.NotEqual(t, created.ResourceVersion, actualUpdated.ResourceVersion)
|
||||
})
|
||||
t.Run("should fail to delete if version does not match", func(t *testing.T) {
|
||||
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
actual, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.GetStaticMetadata().Identifier().Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer("something"),
|
||||
},
|
||||
@@ -477,10 +486,10 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
t.Run("should succeed if version matches", func(t *testing.T) {
|
||||
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
actual, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.GetStaticMetadata().Identifier().Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer(actual.ResourceVersion),
|
||||
},
|
||||
@@ -488,10 +497,10 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("should succeed if version is empty", func(t *testing.T) {
|
||||
actual, err := adminClient.Create(ctx, &template, v1.CreateOptions{})
|
||||
actual, err := adminClient.Create(ctx, &template, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.GetStaticMetadata().Identifier().Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer(actual.ResourceVersion),
|
||||
},
|
||||
@@ -506,7 +515,8 @@ func TestIntegrationPatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTemplateGroupClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
template := v0alpha1.TemplateGroup{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -519,8 +529,10 @@ func TestIntegrationPatch(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
current, err := adminClient.Create(ctx, &template, v1.CreateOptions{})
|
||||
current, err := adminClient.Create(ctx, &template, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
oldClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
|
||||
|
||||
require.NotNil(t, current)
|
||||
require.NotEmpty(t, current.ResourceVersion)
|
||||
|
||||
@@ -531,7 +543,7 @@ func TestIntegrationPatch(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
result, err := adminClient.Patch(ctx, current.Name, types.MergePatchType, []byte(patch), v1.PatchOptions{})
|
||||
result, err := oldClient.Patch(ctx, current.GetStaticMetadata().Identifier().Name, types.MergePatchType, []byte(patch), v1.PatchOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `{{ define "test-another" }} test {{ end }}`, result.Spec.Content)
|
||||
current = result
|
||||
@@ -540,18 +552,15 @@ func TestIntegrationPatch(t *testing.T) {
|
||||
t.Run("should patch with json patch", func(t *testing.T) {
|
||||
expected := `{{ define "test-json-patch" }} test {{ end }}`
|
||||
|
||||
patch := []map[string]interface{}{
|
||||
patch := []resource.PatchOperation{
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/spec/content",
|
||||
"value": expected,
|
||||
Operation: "replace",
|
||||
Path: "/spec/content",
|
||||
Value: expected,
|
||||
},
|
||||
}
|
||||
|
||||
patchData, err := json.Marshal(patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
|
||||
result, err := adminClient.Patch(ctx, current.GetStaticMetadata().Identifier(), resource.PatchRequest{Operations: patch}, resource.PatchOptions{})
|
||||
require.NoError(t, err)
|
||||
expectedSpec := current.Spec
|
||||
expectedSpec.Content = expected
|
||||
@@ -565,7 +574,8 @@ func TestIntegrationListSelector(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTemplateGroupClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
template1 := &v0alpha1.TemplateGroup{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -577,7 +587,7 @@ func TestIntegrationListSelector(t *testing.T) {
|
||||
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
|
||||
},
|
||||
}
|
||||
template1, err := adminClient.Create(ctx, template1, v1.CreateOptions{})
|
||||
template1, err = adminClient.Create(ctx, template1, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
template2 := &v0alpha1.TemplateGroup{
|
||||
@@ -590,7 +600,7 @@ func TestIntegrationListSelector(t *testing.T) {
|
||||
Kind: v0alpha1.TemplateGroupTemplateKindGrafana,
|
||||
},
|
||||
}
|
||||
template2, err = adminClient.Create(ctx, template2, v1.CreateOptions{})
|
||||
template2, err = adminClient.Create(ctx, template2, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
env := helper.GetEnv()
|
||||
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
|
||||
@@ -599,18 +609,18 @@ func TestIntegrationListSelector(t *testing.T) {
|
||||
require.NoError(t, db.SetProvenance(ctx, &definitions.NotificationTemplate{
|
||||
Name: template2.Spec.Title,
|
||||
}, helper.Org1.Admin.Identity.GetOrgID(), "API"))
|
||||
template2, err = adminClient.Get(ctx, template2.Name, v1.GetOptions{})
|
||||
template2, err = adminClient.Get(ctx, template2.GetStaticMetadata().Identifier())
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
tmpls, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
tmpls, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, tmpls.Items, 3) // Includes default template.
|
||||
|
||||
t.Run("should filter by template name", func(t *testing.T) {
|
||||
t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "spec.title=" + template1.Spec.Title,
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{"spec.title=" + template1.Spec.Title},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -618,8 +628,8 @@ func TestIntegrationListSelector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should filter by template metadata name", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "metadata.name=" + template2.Name,
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{"metadata.name=" + template2.Name},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -628,8 +638,8 @@ func TestIntegrationListSelector(t *testing.T) {
|
||||
|
||||
t.Run("should filter by multiple filters", func(t *testing.T) {
|
||||
t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s,spec.title=%s", template2.Name, template2.Spec.Title),
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{fmt.Sprintf("metadata.name=%s,spec.title=%s", template2.Name, template2.Spec.Title)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -637,8 +647,8 @@ func TestIntegrationListSelector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should be empty when filter does not match", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s", "unknown"),
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{fmt.Sprintf("metadata.name=%s", "unknown")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, list.Items)
|
||||
@@ -646,17 +656,17 @@ func TestIntegrationListSelector(t *testing.T) {
|
||||
|
||||
t.Run("should filter by default template name", func(t *testing.T) {
|
||||
t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "spec.title=" + v0alpha1.DefaultTemplateTitle,
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{"spec.title=" + v0alpha1.DefaultTemplateTitle},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
require.Equal(t, templates.DefaultTemplateName, list.Items[0].Name)
|
||||
|
||||
// Now just non-default templates
|
||||
list, err = adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "spec.title!=" + v0alpha1.DefaultTemplateTitle,
|
||||
})
|
||||
list, err = adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{"spec.title!=" + v0alpha1.DefaultTemplateTitle}},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 2)
|
||||
require.NotEqualf(t, templates.DefaultTemplateName, list.Items[0].Name, "Expected non-default template but got %s", list.Items[0].Name)
|
||||
@@ -669,7 +679,8 @@ func TestIntegrationKinds(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
|
||||
client, err := v0alpha1.NewTemplateGroupClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
newTemplate := &v0alpha1.TemplateGroup{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -683,17 +694,17 @@ func TestIntegrationKinds(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("should not let create Mimir template", func(t *testing.T) {
|
||||
_, err := client.Create(ctx, newTemplate, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, newTemplate, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should not let change kind", func(t *testing.T) {
|
||||
newTemplate.Spec.Kind = v0alpha1.TemplateGroupTemplateKindGrafana
|
||||
created, err := client.Create(ctx, newTemplate, v1.CreateOptions{})
|
||||
created, err := client.Create(ctx, newTemplate, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
created.Spec.Kind = v0alpha1.TemplateGroupTemplateKindMimir
|
||||
_, err = client.Update(ctx, created, v1.UpdateOptions{})
|
||||
_, err = client.Update(ctx, created, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -57,7 +58,8 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
client := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
client, err := v0alpha1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
newInterval := &v0alpha1.TimeInterval{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -72,22 +74,22 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
t.Run("create should fail if object name is specified", func(t *testing.T) {
|
||||
interval := newInterval.Copy().(*v0alpha1.TimeInterval)
|
||||
interval.Name = "time-newInterval"
|
||||
_, err := client.Create(ctx, interval, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, interval, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest but got %s", err)
|
||||
})
|
||||
|
||||
var resourceID string
|
||||
var resourceID resource.Identifier
|
||||
t.Run("create should succeed and provide resource name", func(t *testing.T) {
|
||||
actual, err := client.Create(ctx, newInterval, v1.CreateOptions{})
|
||||
actual, err := client.Create(ctx, newInterval, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
require.NotEmptyf(t, actual.UID, "Resource UID should not be empty")
|
||||
resourceID = actual.Name
|
||||
resourceID = actual.GetStaticMetadata().Identifier()
|
||||
})
|
||||
|
||||
var existingInterval *v0alpha1.TimeInterval
|
||||
t.Run("resource should be available by the identifier", func(t *testing.T) {
|
||||
actual, err := client.Get(ctx, resourceID, v1.GetOptions{})
|
||||
actual, err := client.Get(ctx, resourceID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmptyf(t, actual.Name, "Resource name should not be empty")
|
||||
require.Equal(t, newInterval.Spec, actual.Spec)
|
||||
@@ -100,13 +102,13 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
}
|
||||
updated := existingInterval.Copy().(*v0alpha1.TimeInterval)
|
||||
updated.Spec.Name = "another-newInterval"
|
||||
actual, err := client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actual, err := client.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, updated.Spec, actual.Spec)
|
||||
require.NotEqualf(t, updated.Name, actual.Name, "Update should change the resource name but it didn't")
|
||||
require.NotEqualf(t, updated.ResourceVersion, actual.ResourceVersion, "Update should change the resource version but it didn't")
|
||||
|
||||
resource, err := client.Get(ctx, actual.Name, v1.GetOptions{})
|
||||
resource, err := client.Get(ctx, actual.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, actual, resource)
|
||||
})
|
||||
@@ -189,11 +191,13 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
|
||||
client := common.NewTimeIntervalClient(t, tc.user)
|
||||
client, err := v0alpha1.NewTimeIntervalClientFromGenerator(tc.user.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
var expected = &v0alpha1.TimeInterval{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
@@ -209,12 +213,12 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) {
|
||||
|
||||
if tc.canCreate {
|
||||
t.Run("should be able to create time interval", func(t *testing.T) {
|
||||
actual, err := client.Create(ctx, expected, v1.CreateOptions{})
|
||||
actual, err := client.Create(ctx, expected, resource.CreateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
require.Equal(t, expected.Spec, actual.Spec)
|
||||
|
||||
t.Run("should fail if already exists", func(t *testing.T) {
|
||||
_, err := client.Create(ctx, actual, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, actual, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsBadRequest(err), "expected bad request but got %s", err)
|
||||
})
|
||||
|
||||
@@ -222,45 +226,45 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) {
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to create", func(t *testing.T) {
|
||||
_, err := client.Create(ctx, expected, v1.CreateOptions{})
|
||||
_, err := client.Create(ctx, expected, resource.CreateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "Payload %s", string(d))
|
||||
})
|
||||
|
||||
// create resource to proceed with other tests
|
||||
expected, err = adminClient.Create(ctx, expected, v1.CreateOptions{})
|
||||
expected, err = adminClient.Create(ctx, expected, resource.CreateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
require.NotNil(t, expected)
|
||||
}
|
||||
|
||||
if tc.canRead {
|
||||
t.Run("should be able to list time intervals", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
})
|
||||
|
||||
t.Run("should be able to read time interval by resource identifier", func(t *testing.T) {
|
||||
got, err := client.Get(ctx, expected.Name, v1.GetOptions{})
|
||||
got, err := client.Get(ctx, expected.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, got)
|
||||
require.Equal(t, expected.Spec, got.Spec)
|
||||
|
||||
t.Run("should get NotFound if resource does not exist", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
|
||||
_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to list time intervals", func(t *testing.T) {
|
||||
_, err := client.List(ctx, v1.ListOptions{})
|
||||
_, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should be forbidden to read time interval by name", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, expected.Name, v1.GetOptions{})
|
||||
_, err := client.Get(ctx, expected.GetStaticMetadata().Identifier())
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should get forbidden even if name does not exist", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "Notfound", v1.GetOptions{})
|
||||
_, err := client.Get(ctx, resource.Identifier{Namespace: apis.DefaultNamespace, Name: "Notfound"})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
@@ -274,7 +278,7 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) {
|
||||
|
||||
if tc.canUpdate {
|
||||
t.Run("should be able to update time interval", func(t *testing.T) {
|
||||
updated, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
|
||||
updated, err := client.Update(ctx, updatedExpected, resource.UpdateOptions{})
|
||||
require.NoErrorf(t, err, "Payload %s", string(d))
|
||||
|
||||
expected = updated
|
||||
@@ -282,52 +286,54 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) {
|
||||
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
|
||||
up := updatedExpected.Copy().(*v0alpha1.TimeInterval)
|
||||
up.Name = "notFound"
|
||||
_, err := client.Update(ctx, up, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, up, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to update time interval", func(t *testing.T) {
|
||||
_, err := client.Update(ctx, updatedExpected, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, updatedExpected, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should get forbidden even if resource does not exist", func(t *testing.T) {
|
||||
up := updatedExpected.Copy().(*v0alpha1.TimeInterval)
|
||||
up.Name = "notFound"
|
||||
_, err := client.Update(ctx, up, v1.UpdateOptions{})
|
||||
_, err := client.Update(ctx, up, resource.UpdateOptions{
|
||||
ResourceVersion: up.ResourceVersion,
|
||||
})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
deleteOptions := v1.DeleteOptions{Preconditions: &v1.Preconditions{ResourceVersion: util.Pointer(expected.ResourceVersion)}}
|
||||
|
||||
oldClient := common.NewTimeIntervalClient(t, tc.user)
|
||||
if tc.canDelete {
|
||||
t.Run("should be able to delete time interval", func(t *testing.T) {
|
||||
err := client.Delete(ctx, expected.Name, deleteOptions)
|
||||
err := oldClient.Delete(ctx, expected.GetStaticMetadata().Identifier().Name, deleteOptions)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should get NotFound if name does not exist", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
err := oldClient.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
require.Truef(t, errors.IsNotFound(err), "Should get NotFound error but got: %s", err)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
t.Run("should be forbidden to delete time interval", func(t *testing.T) {
|
||||
err := client.Delete(ctx, expected.Name, deleteOptions)
|
||||
err := oldClient.Delete(ctx, expected.GetStaticMetadata().Identifier().Name, deleteOptions)
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
|
||||
t.Run("should be forbidden even if resource does not exist", func(t *testing.T) {
|
||||
err := client.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
err := oldClient.Delete(ctx, "notfound", v1.DeleteOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
})
|
||||
require.NoError(t, adminClient.Delete(ctx, expected.Name, v1.DeleteOptions{}))
|
||||
require.NoError(t, adminClient.Delete(ctx, expected.GetStaticMetadata().Identifier(), resource.DeleteOptions{}))
|
||||
}
|
||||
|
||||
if tc.canRead {
|
||||
t.Run("should get empty list if no mute timings", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
list, err := client.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 0)
|
||||
})
|
||||
@@ -345,7 +351,8 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) {
|
||||
org := helper.Org1
|
||||
|
||||
admin := org.Admin
|
||||
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
env := helper.GetEnv()
|
||||
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
|
||||
@@ -360,7 +367,7 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) {
|
||||
Name: "time-interval-1",
|
||||
TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
|
||||
},
|
||||
}, v1.CreateOptions{})
|
||||
}, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "none", created.GetProvenanceStatus())
|
||||
|
||||
@@ -371,7 +378,7 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) {
|
||||
},
|
||||
}, admin.Identity.GetOrgID(), "API"))
|
||||
|
||||
got, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
got, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "API", got.GetProvenanceStatus())
|
||||
})
|
||||
@@ -379,12 +386,12 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.TimeInterval)
|
||||
updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)
|
||||
|
||||
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
|
||||
t.Run("should not let delete if provisioned", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
|
||||
err := adminClient.Delete(ctx, created.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
}
|
||||
@@ -395,7 +402,9 @@ func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
oldClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
|
||||
interval := v0alpha1.TimeInterval{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -407,21 +416,22 @@ func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
|
||||
created, err := adminClient.Create(ctx, &interval, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
require.NotEmpty(t, created.ResourceVersion)
|
||||
|
||||
t.Run("should forbid if version does not match", func(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.TimeInterval)
|
||||
updated.ResourceVersion = "test"
|
||||
_, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
_, err := adminClient.Update(ctx, updated, resource.UpdateOptions{
|
||||
ResourceVersion: "test",
|
||||
})
|
||||
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
t.Run("should update if version matches", func(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.TimeInterval)
|
||||
updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
|
||||
require.NotEqual(t, updated.ResourceVersion, actualUpdated.ResourceVersion)
|
||||
@@ -431,16 +441,16 @@ func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) {
|
||||
updated.ResourceVersion = ""
|
||||
updated.Spec.TimeIntervals = fakes.IntervalGenerator{}.GenerateMany(2)
|
||||
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
actualUpdated, err := adminClient.Update(ctx, updated, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, updated.Spec, actualUpdated.Spec)
|
||||
require.NotEqual(t, created.ResourceVersion, actualUpdated.ResourceVersion)
|
||||
})
|
||||
t.Run("should fail to delete if version does not match", func(t *testing.T) {
|
||||
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
actual, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.GetStaticMetadata().Identifier().Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer("something"),
|
||||
},
|
||||
@@ -448,10 +458,10 @@ func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) {
|
||||
require.Truef(t, errors.IsConflict(err), "should get Forbidden error but got %s", err)
|
||||
})
|
||||
t.Run("should succeed if version matches", func(t *testing.T) {
|
||||
actual, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
actual, err := adminClient.Get(ctx, created.GetStaticMetadata().Identifier())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.GetStaticMetadata().Identifier().Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer(actual.ResourceVersion),
|
||||
},
|
||||
@@ -459,10 +469,10 @@ func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Run("should succeed if version is empty", func(t *testing.T) {
|
||||
actual, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
|
||||
actual, err := adminClient.Create(ctx, &interval, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = adminClient.Delete(ctx, actual.Name, v1.DeleteOptions{
|
||||
err = oldClient.Delete(ctx, actual.GetStaticMetadata().Identifier().Name, v1.DeleteOptions{
|
||||
Preconditions: &v1.Preconditions{
|
||||
ResourceVersion: util.Pointer(actual.ResourceVersion),
|
||||
},
|
||||
@@ -477,7 +487,9 @@ func TestIntegrationTimeIntervalPatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
oldClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
|
||||
interval := v0alpha1.TimeInterval{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -489,7 +501,7 @@ func TestIntegrationTimeIntervalPatch(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
current, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
|
||||
current, err := adminClient.Create(ctx, &interval, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, current)
|
||||
require.NotEmpty(t, current.ResourceVersion)
|
||||
@@ -501,7 +513,7 @@ func TestIntegrationTimeIntervalPatch(t *testing.T) {
|
||||
}
|
||||
}`
|
||||
|
||||
result, err := adminClient.Patch(ctx, current.Name, types.MergePatchType, []byte(patch), v1.PatchOptions{})
|
||||
result, err := oldClient.Patch(ctx, current.GetStaticMetadata().Identifier().Name, types.MergePatchType, []byte(patch), v1.PatchOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, result.Spec.TimeIntervals)
|
||||
current = result
|
||||
@@ -510,18 +522,15 @@ func TestIntegrationTimeIntervalPatch(t *testing.T) {
|
||||
t.Run("should patch with json patch", func(t *testing.T) {
|
||||
expected := fakes.IntervalGenerator{}.Generate()
|
||||
|
||||
patch := []map[string]interface{}{
|
||||
patch := []resource.PatchOperation{
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/spec/time_intervals/-",
|
||||
"value": expected,
|
||||
Operation: "add",
|
||||
Path: "/spec/time_intervals/-",
|
||||
Value: expected,
|
||||
},
|
||||
}
|
||||
|
||||
patchData, err := json.Marshal(patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
|
||||
result, err := adminClient.Patch(ctx, current.GetStaticMetadata().Identifier(), resource.PatchRequest{Operations: patch}, resource.PatchOptions{})
|
||||
require.NoError(t, err)
|
||||
expectedSpec := v0alpha1.TimeIntervalSpec{
|
||||
Name: current.Spec.Name,
|
||||
@@ -540,7 +549,8 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
interval1 := &v0alpha1.TimeInterval{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
@@ -551,7 +561,7 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
|
||||
TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
|
||||
},
|
||||
}
|
||||
interval1, err := adminClient.Create(ctx, interval1, v1.CreateOptions{})
|
||||
interval1, err = adminClient.Create(ctx, interval1, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
interval2 := &v0alpha1.TimeInterval{
|
||||
@@ -563,7 +573,7 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
|
||||
TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(2),
|
||||
},
|
||||
}
|
||||
interval2, err = adminClient.Create(ctx, interval2, v1.CreateOptions{})
|
||||
interval2, err = adminClient.Create(ctx, interval2, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
env := helper.GetEnv()
|
||||
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
|
||||
@@ -574,18 +584,18 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
|
||||
Name: interval2.Spec.Name,
|
||||
},
|
||||
}, helper.Org1.Admin.Identity.GetOrgID(), "API"))
|
||||
interval2, err = adminClient.Get(ctx, interval2.Name, v1.GetOptions{})
|
||||
interval2, err = adminClient.Get(ctx, interval2.GetStaticMetadata().Identifier())
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
intervals, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
intervals, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, intervals.Items, 2)
|
||||
|
||||
t.Run("should filter by interval name", func(t *testing.T) {
|
||||
t.Skip("disabled until app installer supports it") // TODO revisit when custom field selectors are supported
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "spec.name=" + interval1.Spec.Name,
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{"spec.name=" + interval1.Spec.Name},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -593,8 +603,8 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should filter by interval metadata name", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: "metadata.name=" + interval2.Name,
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{"metadata.name=" + interval2.Name},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -603,8 +613,8 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
|
||||
|
||||
t.Run("should filter by multiple filters", func(t *testing.T) {
|
||||
t.Skip("disabled until app installer supports it")
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s,spec.name=%s", interval2.Name, interval2.Spec.Name),
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{fmt.Sprintf("metadata.name=%s", interval2.Name), fmt.Sprintf("spec.name=%s", interval2.Spec.Name)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list.Items, 1)
|
||||
@@ -612,8 +622,8 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("should be empty when filter does not match", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{
|
||||
FieldSelector: fmt.Sprintf("metadata.name=%s", "unknown"),
|
||||
list, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{
|
||||
FieldSelectors: []string{fmt.Sprintf("metadata.name=%s", "unknown")},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, list.Items)
|
||||
@@ -647,18 +657,20 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
v1intervals, err := timeinterval.ConvertToK8sResources(orgID, mtis, func(int64) string { return "default" }, nil)
|
||||
require.NoError(t, err)
|
||||
for _, interval := range v1intervals.Items {
|
||||
_, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
|
||||
_, err := adminClient.Create(ctx, &interval, resource.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
|
||||
routeClient, err := v0alpha1.NewRoutingTreeClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
|
||||
require.NoError(t, err)
|
||||
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
|
||||
_, err = routeClient.Update(ctx, v1route, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
|
||||
@@ -675,7 +687,7 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
currentRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
|
||||
require.Equal(t, http.StatusAccepted, status)
|
||||
|
||||
intervals, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
intervals, err := adminClient.List(ctx, apis.DefaultNamespace, resource.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, intervals.Items, 3)
|
||||
intervalIdx := slices.IndexFunc(intervals.Items, func(interval v0alpha1.TimeInterval) bool {
|
||||
@@ -700,7 +712,7 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
renamed := interval.Copy().(*v0alpha1.TimeInterval)
|
||||
renamed.Spec.Name += "-new"
|
||||
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
actual, err := adminClient.Update(ctx, renamed, resource.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
|
||||
@@ -732,20 +744,20 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, db.DeleteProvenance(ctx, ¤tRoute, orgID))
|
||||
})
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
actual, err := adminClient.Update(ctx, renamed, resource.UpdateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
t.Run("provisioned rules", func(t *testing.T) {
|
||||
ruleUid := currentRuleGroup.Rules[0].GrafanaManagedAlert.UID
|
||||
resource := &ngmodels.AlertRule{UID: ruleUid}
|
||||
require.NoError(t, db.SetProvenance(ctx, resource, orgID, "API"))
|
||||
rule := &ngmodels.AlertRule{UID: ruleUid}
|
||||
require.NoError(t, db.SetProvenance(ctx, rule, orgID, "API"))
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, db.DeleteProvenance(ctx, resource, orgID))
|
||||
require.NoError(t, db.DeleteProvenance(ctx, rule, orgID))
|
||||
})
|
||||
|
||||
actual, err := adminClient.Update(ctx, renamed, v1.UpdateOptions{})
|
||||
actual, err := adminClient.Update(ctx, renamed, resource.UpdateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result: %v", actual)
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
@@ -754,7 +766,7 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("should fail to delete if time interval is used in rule and routes", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, interval.Name, v1.DeleteOptions{})
|
||||
err := adminClient.Delete(ctx, interval.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
@@ -763,7 +775,7 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
route.Routes[0].MuteTimeIntervals = nil
|
||||
legacyCli.UpdateRoute(t, route, true)
|
||||
|
||||
err = adminClient.Delete(ctx, interval.Name, v1.DeleteOptions{})
|
||||
err = adminClient.Delete(ctx, interval.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
|
||||
@@ -773,7 +785,7 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
|
||||
})
|
||||
intervalToDelete := intervals.Items[idx]
|
||||
|
||||
err = adminClient.Delete(ctx, intervalToDelete.Name, v1.DeleteOptions{})
|
||||
err = adminClient.Delete(ctx, intervalToDelete.GetStaticMetadata().Identifier(), resource.DeleteOptions{})
|
||||
require.Truef(t, errors.IsConflict(err), "Expected Conflict, got: %s", err)
|
||||
})
|
||||
})
|
||||
@@ -785,7 +797,8 @@ func TestIntegrationTimeIntervalValidation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
helper := getTestHelper(t)
|
||||
|
||||
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
|
||||
adminClient, err := v0alpha1.NewTimeIntervalClientFromGenerator(helper.Org1.Admin.GetClientRegistry())
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -819,7 +832,7 @@ func TestIntegrationTimeIntervalValidation(t *testing.T) {
|
||||
},
|
||||
Spec: tc.interval,
|
||||
}
|
||||
_, err := adminClient.Create(ctx, i, v1.CreateOptions{})
|
||||
_, err := adminClient.Create(ctx, i, resource.CreateOptions{})
|
||||
require.Error(t, err)
|
||||
require.Truef(t, errors.IsBadRequest(err), "Expected BadRequest, got: %s", err)
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
githubConnection "github.com/grafana/grafana/apps/provisioning/pkg/connection/github"
|
||||
appsdk_k8s "github.com/grafana/grafana-app-sdk/k8s"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -28,6 +28,8 @@ import (
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
githubConnection "github.com/grafana/grafana/apps/provisioning/pkg/connection/github"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/configprovider"
|
||||
@@ -57,6 +59,8 @@ import (
|
||||
const (
|
||||
Org1 = "Org1"
|
||||
Org2 = "OrgB"
|
||||
|
||||
DefaultNamespace = "default"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -445,6 +449,11 @@ func (c *User) RESTClient(t *testing.T, gv *schema.GroupVersion) *rest.RESTClien
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *User) GetClientRegistry() *appsdk_k8s.ClientRegistry {
|
||||
restConfig := c.NewRestConfig()
|
||||
return appsdk_k8s.NewClientRegistry(*restConfig, appsdk_k8s.DefaultClientConfig())
|
||||
}
|
||||
|
||||
type RequestParams struct {
|
||||
User User
|
||||
Method string // GET, POST, PATCH, etc
|
||||
|
||||
Reference in New Issue
Block a user