Merge branch 'main' into drclau/unistor/replace-authenticators-3

This commit is contained in:
Claudiu Dragalina-Paraipan 2024-08-07 14:42:39 +03:00
commit 0fdd2ff802
299 changed files with 5202 additions and 3047 deletions

4
.github/CODEOWNERS vendored
View File

@ -74,6 +74,7 @@
/apps/alerting/ @grafana/alerting-backend
/pkg/api/ @grafana/grafana-backend-group
/pkg/apis/ @grafana/grafana-app-platform-squad
/pkg/apis/alerting_notifications @grafana/grafana-app-platform-squad @grafana/alerting-backend @grafana/alerting-frontend
/pkg/bus/ @grafana/grafana-search-and-storage
/pkg/cmd/ @grafana/grafana-backend-group
/pkg/cmd/grafana/apiserver @grafana/grafana-app-platform-squad
@ -101,6 +102,7 @@
/pkg/middleware/ @grafana/grafana-backend-group
/pkg/mocks/ @grafana/grafana-backend-group
/pkg/models/ @grafana/grafana-backend-group
/pkg/semconv/ @grafana/grafana-backend-group
/pkg/server/ @grafana/grafana-backend-group
/pkg/apiserver @grafana/grafana-app-platform-squad
/pkg/apimachinery @grafana/grafana-app-platform-squad
@ -149,6 +151,7 @@
/pkg/setting/ @grafana/grafana-backend-services-squad
/pkg/tests/ @grafana/grafana-backend-services-squad
/pkg/tests/apis/ @grafana/grafana-app-platform-squad
/pkg/tests/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend
/pkg/tests/api/correlations/ @grafana/explore-squad
/pkg/tsdb/grafanads/ @grafana/grafana-backend-group
/pkg/tsdb/opentsdb/ @grafana/partner-datasources
@ -650,6 +653,7 @@ embed.go @grafana/grafana-as-code
/pkg/kinds/ @grafana/grafana-as-code
/pkg/registry/ @grafana/grafana-as-code
/pkg/registry/apis/ @grafana/grafana-app-platform-squad
/pkg/registry/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend
/pkg/codegen/ @grafana/grafana-as-code
/pkg/codegen/generators @grafana/grafana-as-code
/pkg/kinds/*/*_gen.go @grafana/grafana-as-code

View File

@ -27,6 +27,6 @@ jobs:
with:
version: v1.59.1
args: |
--config .golangci.toml --max-same-issues=0 --max-issues-per-linter=0 --verbose ./pkg/... ./pkg/apiserver/... ./pkg/apimachinery/... ./pkg/promlib/...
--config .golangci.toml --max-same-issues=0 --max-issues-per-linter=0 --verbose ./pkg/... ./pkg/apiserver/... ./pkg/apimachinery/... ./pkg/promlib/... ./pkg/semconv/... ./pkg/storage/unified/resource/...
skip-cache: true
install-mode: binary

File diff suppressed because one or more lines are too long

925
.yarn/releases/yarn-4.4.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
spec: 'https://mskelton.dev/yarn-outdated/v2'
yarnPath: .yarn/releases/yarn-4.3.1.cjs
yarnPath: .yarn/releases/yarn-4.4.0.cjs
# Uncomment the following lines if you want to use Verdaccio local npm registry. Read more at packages/README.md
#npmScopes:
# grafana:

View File

@ -61,6 +61,7 @@ COPY pkg/build/go.* pkg/build/
COPY pkg/build/wire/go.* pkg/build/wire/
COPY pkg/promlib/go.* pkg/promlib/
COPY pkg/storage/unified/resource/go.* pkg/storage/unified/resource/
COPY pkg/semconv/go.* pkg/semconv/
RUN go mod download
RUN if [[ "$BINGO" = "true" ]]; then \

View File

@ -10,7 +10,7 @@ include .bingo/Variables.mk
GO = go
GO_VERSION = 1.22.4
GO_FILES ?= ./pkg/... ./pkg/apiserver/... ./pkg/apimachinery/... ./pkg/promlib/...
GO_FILES ?= ./pkg/... ./pkg/apiserver/... ./pkg/apimachinery/... ./pkg/promlib/... ./pkg/semconv/... ./pkg/storage/unified/resource/...
SH_FILES ?= $(shell find ./scripts -name *.sh)
GO_RACE := $(shell [ -n "$(GO_RACE)" -o -e ".go-race-enabled-locally" ] && echo 1 )
GO_RACE_FLAG := $(if $(GO_RACE),-race)

View File

@ -65,6 +65,7 @@ enable_gzip = false
# https certs & key file
cert_file =
cert_key =
cert_pass =
# Certificates file watch interval
certs_watch_interval =

View File

@ -67,6 +67,9 @@
;cert_file =
;cert_key =
# optional password to be used to decrypt key file
;cert_pass =
# Certificates file watch interval
;certs_watch_interval =

View File

@ -68,7 +68,7 @@ Examples of labels are `server=server1` or `team=backend`. Each alert rule can h
For example, an alert instance might have the label set `{alertname="High CPU usage",server="server1"}` while another alert instance might have the label set `{alertname="High CPU usage",server="server2"}`. These are two separate alert instances because although their `alertname` labels are the same, their `server` labels are different.
{{< figure alt="Image shows an example of an alert instance and its labels" src="/static/img/docs/alerting/unified/multi-dimensional-alert.png" >}}
{{< figure alt="Image shows an example of an alert instance and the labels used on the alert instance." src="/static/img/docs/alerting/unified/multi-dimensional-alert.png" >}}
Labels are a fundamental component of alerting:

View File

@ -131,7 +131,7 @@ Notification templates represent the alternative approach to templating designed
Here is an example of a notification template:
```go
{ define "alerts.message" -}}
{{ define "alerts.message" -}}
{{ if .Alerts.Firing -}}
{{ len .Alerts.Firing }} firing alert(s)
{{ template "alerts.summarize" .Alerts.Firing }}

View File

@ -111,5 +111,3 @@ To access the State history view, complete the following steps.
The value shown for each instance is for each part of the expression that was evaluated.
1. Click the labels to filter and narrow down the results.
{{< figure src="/media/docs/alerting/state-history.png" max-width="750px" >}}

View File

@ -58,6 +58,8 @@ refs:
# Configure the Tempo data source
The Tempo data source sets how Grafana connects to your Tempo database and lets you configure features and integrations with other telemetry signals.
To configure basic settings for the Tempo data source, complete the following steps:
1. Click **Connections** in the left-side menu.
@ -72,7 +74,7 @@ To configure basic settings for the Tempo data source, complete the following st
| **Name** | Sets the name you use to refer to the data source in panels and queries. |
| **Default** | Sets the data source that's pre-selected for new panels. |
| **URL** | Sets the URL of the Tempo instance, such as `http://tempo`. |
| **Basic Auth** | Enables basic authentication to the Tempo data source. |
| **Basic Auth** | Enables authentication to the Tempo data source. |
| **User** | Sets the user name for basic authentication. |
| **Password** | Sets the password for basic authentication. |
@ -82,6 +84,33 @@ This video explains how to add data sources, including Loki, Tempo, and Mimir, t
{{< youtube id="cqHO0oYW6Ic" start="298" >}}
## Streaming
<!-- The traceQLStreaming toggle will be deprecated in Grafana 11.2 and removed in 11.3. -->
Streaming enables TraceQL query results to be displayed as they become available. Without streaming, no results are displayed until all results have returned.
{{< docs/public-preview product="TraceQL streaming results" >}}
### Requirements
To use streaming, you need to:
- Be running Tempo version 2.2 or newer, or Grafana Enterprise Traces (GET) version 2.2 or newer, or be using Grafana Cloud Traces.
- For self-managed Tempo or GET instances: If your Tempo or GET instance is behind a load balancer or proxy that doesn't supporting gRPC or HTTP2, streaming may not work and should be disabled.
### Activate streaming
For streaming to work for a particular Tempo data source, set your Grafana's `traceQLStreaming` [feature toggle](https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/feature-toggles/) to true and set **Streaming** to enabled in your Tempo data source configuration.
![Streaming section in Tempo data source](/media/docs/grafana/data-sources/tempo-data-source-streaming-v11.2.png)
If you are using Grafana Cloud, the `traceQLStreaming` feature toggle is already set to `true` by default.
If the Tempo data source is set to allow streaming but the `traceQLStreaming` feature toggle is set to `false` in Grafana, no streaming will occur.
If the data source has streaming disabled and `traceQLStreaming` is set to `true`, no streaming will happen for that data source.
## Trace to logs
The **Trace to logs** setting configures [trace to logs](ref:explore-trace-integration) that's available when you integrate Grafana with Tempo.
@ -208,7 +237,8 @@ To use custom queries with the configuration, follow these steps:
## Custom query variables
To use a variable in your trace to logs, metrics or profiles you need to wrap it in `${}`. For example, `${__span.name}`.
To use a variable in your trace to logs, metrics, or profiles, you need to wrap it in `${}`.
For example, `${__span.name}`.
| Variable name | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

View File

@ -6,7 +6,8 @@ labels:
- oss
title: Explore Metrics
aliases:
description: This topic describes the Explore Metrics feature
canonical: https://grafana.com/docs/grafana/latest/explore/explore-metrics/
description: Explore Metrics lets you browse Prometheus-compatible metrics using an intuitive, queryless experience.
weight: 200
---

View File

@ -64,6 +64,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `lokiQueryHints` | Enables query hints for Loki | Yes |
| `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | |
| `groupToNestedTableTransformation` | Enables the group to nested table transformation | Yes |
| `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes |
| `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes |
| `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | |
| `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes |

View File

@ -6,29 +6,15 @@ labels:
title: 'Alerting Provisioning HTTP API '
---
The Alerting provisioning API can be used to create, modify, and delete resources relevant to [Grafana-managed alerts]({{< relref "/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule" >}}). It is the one used by our [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs).
The Alerting Provisioning HTTP API can be used to create, modify, and delete resources relevant to Grafana-managed alerts. This API is the one used by our [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs).
To manage resources related to [data source-managed alerts]({{< relref "/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule" >}}), including recording rules, use the [Mimir tool](https://grafana.com/docs/mimir/latest/manage/tools/mimirtool/) and [Cortex tool](https://github.com/grafana/cortex-tools#cortextool).
For more information on the differences between Grafana-managed and data source-managed alerts, refer to [Introduction to alert rules](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/).
## Information
> If you are running Grafana Enterprise, you need to add specific permissions for some endpoints. For more information, refer to [Role-based access control permissions](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/).
### Version
## Grafana-managed endpoints
1.1.0
## Content negotiation
### Consumes
- application/json
### Produces
- application/json
- text/yaml
- application/yaml
## All endpoints
Note that the JSON format from most of the following endpoints is not fully compatible with [provisioning via configuration JSON files](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning/).
### Alert rules
@ -45,13 +31,18 @@ To manage resources related to [data source-managed alerts]({{< relref "/docs/gr
| GET | /api/v1/provisioning/alert-rules | [route get alert rules](#route-get-alert-rules) | Get all the alert rules. |
| GET | /api/v1/provisioning/alert-rules/export | [route get alert rules export](#route-get-alert-rules-export) | Export all alert rules in provisioning file format. |
#### Example alert rules template
**Example request for new alert rule:**
```http
POST /api/v1/provisioning/alert-rules
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```json
{
"title": "TEST-API_1",
"ruleGroup": "API",
"folderUID": "FOLDER",
"folderUID": "SET_FOLDER_UID",
"noDataState": "OK",
"execErrState": "OK",
"for": "5m",
@ -72,7 +63,7 @@ To manage resources related to [data source-managed alerts]({{< relref "/docs/gr
"from": 600,
"to": 0
},
"datasourceUid": " XXXXXXXXX-XXXXXXXXX-XXXXXXXXXX",
"datasourceUid": "XXXXXXXXX-XXXXXXXXX-XXXXXXXXXX",
"model": {
"expr": "up",
"hide": false,
@ -122,6 +113,99 @@ To manage resources related to [data source-managed alerts]({{< relref "/docs/gr
}
]
}
```
#### Example Response:
```http
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 1,
"uid": "XXXXXXXXX",
"orgID": 1,
"folderUID": "SET_FOLDER_UID",
"ruleGroup": "API3",
"title": "TEST-API_1",
"condition": "B",
"data": [
{
"refId": "A",
"queryType": "",
"relativeTimeRange": {
"from": 600,
"to": 0
},
"datasourceUid": "XXXXXXXXX-XXXXXXXXX-XXXXXXXXXX",
"model": {
"expr": "up",
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "A"
}
},
{
"refId": "B",
"queryType": "",
"relativeTimeRange": {
"from": 0,
"to": 0
},
"datasourceUid": "-100",
"model": {
"conditions": [
{
"evaluator": {
"params": [
6
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A"
]
},
"reducer": {
"params": [],
"type": "last"
},
"type": "query"
}
],
"datasource": {
"type": "__expr__",
"uid": "-100"
},
"hide": false,
"intervalMs": 1000,
"maxDataPoints": 43200,
"refId": "B",
"type": "classic_conditions"
}
}
],
"updated": "2024-08-02T13:19:32.609640048Z",
"noDataState": "OK",
"execErrState": "OK",
"for": "5m",
"annotations": {
"summary": "test_api_1"
},
"labels": {
"API": "test1"
},
"provenance": "api",
"isPaused": false,
"notification_settings": null,
"record": null
}
```
### Contact points
@ -134,6 +218,34 @@ To manage resources related to [data source-managed alerts]({{< relref "/docs/gr
| PUT | /api/v1/provisioning/contact-points/:uid | [route put contactpoint](#route-put-contactpoint) | Update an existing contact point. |
| GET | /api/v1/provisioning/contact-points/export | [route get contactpoints export](#route-get-contactpoints-export) | Export all contact points in provisioning file format. |
**Example Request for all the contact points:**
```http
GET /api/v1/provisioning/contact-points
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response:**
```http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"uid": "",
"name": "email receiver",
"type": "email",
"settings": {
"addresses": "<example@email.com>"
},
"disableResolveMessage": false
}
]
```
### Notification policies
| Method | URI | Name | Summary |
@ -143,6 +255,38 @@ To manage resources related to [data source-managed alerts]({{< relref "/docs/gr
| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. |
| GET | /api/v1/provisioning/policies/export | [route get policy tree export](#route-get-policy-tree-export) | Export the notification policy tree in provisioning file format. |
**Example Request for exporting the notification policy tree in YAML format:**
```http
GET /api/v1/provisioning/policies/export?format=yaml
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response:**
```http
HTTP/1.1 200 OK
Content-Type: text/yaml
apiVersion: 1
policies:
- orgId: 1
receiver: My Contact Email Point
group_by:
- grafana_folder
- alertname
routes:
- receiver: My Contact Email Point
object_matchers:
- - monitor
- =
- testdata
mute_time_intervals:
- weekends
```
### Mute timings
| Method | URI | Name | Summary |
@ -155,6 +299,38 @@ To manage resources related to [data source-managed alerts]({{< relref "/docs/gr
| GET | /api/v1/provisioning/mute-timings/export | [route get mute timings export](#route-get-mute-timings-export) | Export all mute timings in provisioning file format. |
| GET | /api/v1/provisioning/mute-timings/:name/export | [route get mute timing export](#route-get-mute-timing-export) | Export a mute timing in provisioning file format. |
**Example Request for all mute timings:**
```http
GET /api/v1/provisioning/mute-timings
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response:**
```http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"name": "weekends",
"time_intervals": [
{
"weekdays": [
"saturday",
"sunday"
]
}
],
"version": "",
"provenance": "file"
}
]
```
### Templates
| Method | URI | Name | Summary |
@ -164,7 +340,36 @@ To manage resources related to [data source-managed alerts]({{< relref "/docs/gr
| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. |
| PUT | /api/v1/provisioning/templates/:name | [route put template](#route-put-template) | Create or update a notification template. |
## Edit resources in the Grafana UI
**Example Request for all notification templates:**
```http
GET /api/v1/provisioning/templates
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response:**
```http
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"name": "custom_email.message",
"template": "{{ define \"custom_email.message\" }}\n Custom alert!\n{{ end }}",
"provenance": "file"
},
{
"name": "custom_email.subject",
"template": "{{ define \"custom_email.subject\" }}\n{{ len .Alerts.Firing }} firing alert(s), {{ len .Alerts.Resolved }} resolved alert(s)\n{{ end }}",
"provenance": "file"
}
]
```
### Edit resources in the Grafana UI
By default, you cannot edit API-provisioned alerting resources in Grafana. To enable editing these resources in the Grafana UI, add the `X-Disable-Provenance` header to the following requests in the API:
@ -177,6 +382,14 @@ By default, you cannot edit API-provisioned alerting resources in Grafana. To en
To reset the notification policy tree to the default and unlock it for editing in the Grafana UI, use the `DELETE /api/v1/provisioning/policies` endpoint.
## Data source-managed resources
The Alerting Provisioning HTTP API can only be used to manage Grafana-managed alert resources. To manage resources related to [data source-managed alerts](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/create-mimir-loki-managed-rule/), consider the following tools:
- [mimirtool](https://grafana.com/docs/mimir/<GRAFANA_VERSION>/manage/tools/mimirtool/): to interact with the Mimir alertmanager and ruler configuration.
- [cortex-tools](https://github.com/grafana/cortex-tools#cortextool): to interact with the Cortex alertmanager and ruler configuration.
- [lokitool](https://grafana.com/docs/loki/<GRAFANA_VERSION>/alert/#lokitool): to configure the Loki Ruler.
## Paths
### <span id="route-delete-alert-rule"></span> Delete a specific alert rule by UID. (_RouteDeleteAlertRule_)
@ -216,10 +429,6 @@ Status: No Content
DELETE /api/v1/provisioning/contact-points/:uid
```
#### Consumes
- application/json
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
@ -884,10 +1093,6 @@ Status: Not Found
POST /api/v1/provisioning/alert-rules
```
#### Consumes
- application/json
#### Parameters
{{% responsive-table %}}
@ -930,10 +1135,6 @@ Status: Bad Request
POST /api/v1/provisioning/contact-points
```
#### Consumes
- application/json
#### Parameters
{{% responsive-table %}}
@ -976,10 +1177,6 @@ Status: Bad Request
POST /api/v1/provisioning/mute-timings
```
#### Consumes
- application/json
#### Parameters
{{% responsive-table %}}
@ -1022,10 +1219,6 @@ Status: Bad Request
PUT /api/v1/provisioning/alert-rules/:uid
```
#### Consumes
- application/json
#### Parameters
{{% responsive-table %}}
@ -1069,10 +1262,6 @@ Status: Bad Request
PUT /api/v1/provisioning/folder/:folderUid/rule-groups/:group
```
#### Consumes
- application/json
#### Parameters
{{% responsive-table %}}
@ -1117,10 +1306,6 @@ Status: Bad Request
PUT /api/v1/provisioning/contact-points/:uid
```
#### Consumes
- application/json
#### Parameters
{{% responsive-table %}}
@ -1164,10 +1349,6 @@ Status: Bad Request
PUT /api/v1/provisioning/mute-timings/:name
```
#### Consumes
- application/json
#### Parameters
{{% responsive-table %}}
@ -1211,10 +1392,6 @@ Status: Bad Request
PUT /api/v1/provisioning/policies
```
#### Consumes
- application/json
#### Parameters
{{% responsive-table %}}
@ -1257,10 +1434,6 @@ Status: Bad Request
PUT /api/v1/provisioning/templates/:name
```
#### Consumes
- application/json
{{% responsive-table %}}
#### Parameters
@ -1304,10 +1477,6 @@ Status: Bad Request
DELETE /api/v1/provisioning/policies
```
#### Consumes
- application/json
#### All responses
| Code | Status | Description | Has headers | Schema |

View File

@ -161,9 +161,13 @@ Queries can take a little while to return results. The results appear in a table
The Tempo data source supports streaming responses to TraceQL queries so you can see partial query results as they come in without waiting for the whole query to finish.
{{% admonition type="note" %}}
To use this experimental feature, enable the `traceQLStreaming` feature toggle. If youre using Grafana Cloud and would like to enable this feature, please contact customer support.
To use this public preview feature, enable the `traceQLStreaming` feature toggle.
When active, all configured Tempo data sources will attempt to use streaming.
You can control which Tempo data sources do and don't attempt to stream results at the per-data source level using the **Streaming** section of the Tempo data source configuration.
{{% /admonition %}}
Streaming is available for both the **Search** and **TraceQL** query types, and you'll get immediate visibility of incoming traces on the results table.
Streaming is available for both the **Search** and **TraceQL** query types.
You'll get immediate visibility of incoming traces on the results table.
{{< video-embed src="/media/docs/grafana/data-sources/tempo-streaming-v2.mp4" >}}

7
go.mod
View File

@ -82,7 +82,7 @@ require (
github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics
github.com/grafana/dskit v0.0.0-20240311184239-73feada6c0d7 // @grafana/grafana-backend-group
github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 // @grafana/grafana-operator-experience-squad
github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana-aws-sdk v0.30.0 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0 // @grafana/partner-datasources
github.com/grafana/grafana-cloud-migration-snapshot v1.2.0 // @grafana/grafana-operator-experience-squad
@ -472,7 +472,10 @@ require (
require github.com/phpdave11/gofpdi v1.0.13 // @grafana/sharing-squad
require github.com/google/go-querystring v1.1.0 // indirect
require (
github.com/google/go-querystring v1.1.0 // indirect; @grafana/observability-metrics
github.com/grafana/e2e v0.1.1 // indirect; @grafana-app-platform-squad
)
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240523142256-cc370b98af7c

18
go.sum
View File

@ -2320,10 +2320,28 @@ github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6k
github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU=
github.com/grafana/dskit v0.0.0-20240311184239-73feada6c0d7 h1:yd9yoNgEOtp8O0MbtqXoMVqr+ZbU4oZFE8a04z8WXFE=
github.com/grafana/dskit v0.0.0-20240311184239-73feada6c0d7/go.mod h1:RpTvZ9nkdXqyQro5DULQHJl9B6vwvEj95Dk6WIXqTLQ=
github.com/grafana/e2e v0.1.1 h1:/b6xcv5BtoBnx8cZnCiey9DbjEc8z7gXHO5edoeRYxc=
github.com/grafana/e2e v0.1.1/go.mod h1:RpNLgae5VT+BUHvPE+/zSypmOXKwEu4t+tnEMS1ATaE=
github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 h1:jxJJ5z0GxqhWFbQUsys3BHG8jnmniJ2Q74tXAG1NaDo=
github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447/go.mod h1:IxsY6mns6Q5sAnWcrptrgUrSglTZJXH/kXr9nbpb/9I=
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 h1:X8IKQ0wu40wpvYcKfBcc5T4QnhdQjUhtUtB/1CY89lE=
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU=
github.com/grafana/gomemcache v0.0.0-20240801211247-70ab5c5d3cfe h1:wLBInGo6gCp/gmgyVpk76RtSbRpjiic5XZF1CwEsNPs=
github.com/grafana/gomemcache v0.0.0-20240801211247-70ab5c5d3cfe/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
github.com/grafana/gomemcache v0.0.0-20240802030650-a3dfe179e761 h1:IkOfxkgOwVRun96IWwqH1Qgb+FSOz5GEHz0+DPHKAVk=
github.com/grafana/gomemcache v0.0.0-20240802030650-a3dfe179e761/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
github.com/grafana/gomemcache v0.0.0-20240802031600-ac7e88487c0b h1:UAc4nkD8lSdSxuusEV9s/1h9VrIOD8GwPNd1PEQR8+4=
github.com/grafana/gomemcache v0.0.0-20240802031600-ac7e88487c0b/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
github.com/grafana/gomemcache v0.0.0-20240802032403-45805b5fe293 h1:5CffDaSN2oArwTdUYfwX9YMxsUUTaJMoCeNy9kE2HCM=
github.com/grafana/gomemcache v0.0.0-20240802032403-45805b5fe293/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
github.com/grafana/gomemcache v0.0.0-20240802032810-8f21fdbcb39d h1:f6+eE8zygAcjBs761JSuSfSHHzW0PdMFahljsTijw+w=
github.com/grafana/gomemcache v0.0.0-20240802032810-8f21fdbcb39d/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
github.com/grafana/gomemcache v0.0.0-20240802041632-201cbbb2bceb h1:+ZqYDh5tQqppvqvmi+u4A+H/1tgXacV7MeEijPqPSj4=
github.com/grafana/gomemcache v0.0.0-20240802041632-201cbbb2bceb/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
github.com/grafana/gomemcache v0.0.0-20240803024453-e764d08ea2ed h1:tVRLkqWy0qJOpZRwdwqDpKoJyH3uTnJ4w3JwrVX5Ac4=
github.com/grafana/gomemcache v0.0.0-20240803024453-e764d08ea2ed/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e h1:UlEET0InuoFautfaFp8lDrNF7rPHYXuBMrzwWx9XqFY=
github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU=
github.com/grafana/grafana-aws-sdk v0.30.0 h1:6IIetM4s2NbvPOI4/fefsyN84BIb0/T09lHGF/pywo8=
github.com/grafana/grafana-aws-sdk v0.30.0/go.mod h1:ZSVPU7IIJSi5lEg+K3Js+EUpZLXxUaBdaQWH+As1ihI=
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0 h1:lajVqTWaE96MpbjZToj7EshvqgRWOfYNkD4MbIZizaY=

View File

@ -7,6 +7,7 @@ use (
./pkg/build
./pkg/build/wire
./pkg/promlib
./pkg/semconv
./pkg/storage/unified/resource
./pkg/util/xorm
)

View File

@ -315,6 +315,8 @@ github.com/eapache/go-resiliency v1.6.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b h1:ZHiD4/yE4idlbqvAO6iYCOYRzOMRpxkW+FKasRA3tsQ=
github.com/efficientgo/tools/core v0.0.0-20220225185207-fe763185946b/go.mod h1:OmVcnJopJL8d3X3sSXTiypGoUSgFq1aDGmlrdi9dn/M=
github.com/elastic/go-sysinfo v1.11.2 h1:mcm4OSYVMyws6+n2HIVMGkln5HOpo5Ie1ZmbbNn0jg4=
github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJa7AfT8HpBFQ=
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
@ -484,6 +486,8 @@ github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3ro
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8=
@ -661,6 +665,8 @@ github.com/tdewolff/minify/v2 v2.12.9 h1:dvn5MtmuQ/DFMwqf5j8QhEVpPX6fi3WGImhv8RU
github.com/tdewolff/minify/v2 v2.12.9/go.mod h1:qOqdlDfL+7v0/fyymB+OP497nIxJYSvX4MQWA8OoiXU=
github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvFMA=
github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
github.com/thanos-io/objstore v0.0.0-20220809103346-8ef1f215e2bf h1:onQsPyHlq2yIWU+Nfl6yStuqnZuVQQN8FZ8sBb2wqtw=
github.com/thanos-io/objstore v0.0.0-20220809103346-8ef1f215e2bf/go.mod h1:v0NhuxxxUFUPatQcVNSCUkBEVezXzl7LSdaBOZygq98=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@ -829,6 +835,8 @@ golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhp
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
set -o errexit
set -o nounset
@ -8,11 +8,12 @@ rm -rf data/grafana-aggregator
mkdir -p data/grafana-aggregator
openssl req -nodes -new -x509 -keyout data/grafana-aggregator/ca.key -out data/grafana-aggregator/ca.crt
openssl req -nodes -new -x509 -keyout data/grafana-aggregator/ca.key -out data/grafana-aggregator/ca.crt \
-subj "/C=US/ST=New Sweden/L=Stockholm /O=Grafana/OU=R&D/CN=test-ca/emailAddress=test@grafana.app" -days 3650
openssl req -out data/grafana-aggregator/client.csr -new -newkey rsa:4096 -nodes -keyout data/grafana-aggregator/client.key \
-subj "/CN=development/O=system:masters" \
-addext "extendedKeyUsage = clientAuth"
openssl x509 -req -days 365 -in data/grafana-aggregator/client.csr -CA data/grafana-aggregator/ca.crt -CAkey data/grafana-aggregator/ca.key \
openssl x509 -req -days 3650 -in data/grafana-aggregator/client.csr -CA data/grafana-aggregator/ca.crt -CAkey data/grafana-aggregator/ca.key \
-set_serial 01 \
-sha256 -out data/grafana-aggregator/client.crt \
-copy_extensions=copyall
@ -21,7 +22,11 @@ openssl req -out data/grafana-aggregator/server.csr -new -newkey rsa:4096 -nodes
-subj "/CN=localhost/O=aggregated" \
-addext "subjectAltName = DNS:v0alpha1.example.grafana.app.default.svc,DNS:localhost" \
-addext "extendedKeyUsage = serverAuth, clientAuth"
openssl x509 -req -days 365 -in data/grafana-aggregator/server.csr -CA data/grafana-aggregator/ca.crt -CAkey data/grafana-aggregator/ca.key \
openssl x509 -req -days 3650 -in data/grafana-aggregator/server.csr -CA data/grafana-aggregator/ca.crt -CAkey data/grafana-aggregator/ca.key \
-set_serial 02 \
-sha256 -out data/grafana-aggregator/server.crt \
-copy_extensions=copyall
# Apply broad permissions to certificates/keys so that containers passing these around for
# tests don't run into permission related errors
chmod 755 data/grafana-aggregator/*.*

View File

@ -148,10 +148,10 @@
"@types/uuid": "9.0.8",
"@types/webpack-assets-manifest": "^5",
"@types/webpack-env": "^1.18.4",
"@types/yargs": "17.0.32",
"@types/yargs": "17.0.33",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"autoprefixer": "10.4.19",
"autoprefixer": "10.4.20",
"babel-loader": "9.1.3",
"blob-polyfill": "7.0.20220408",
"browserslist": "^4.21.4",
@ -172,6 +172,7 @@
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "28.6.0",
"eslint-plugin-jest-dom": "^5.4.0",
"eslint-plugin-jsdoc": "48.11.0",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-lodash": "7.4.0",
@ -197,14 +198,14 @@
"jest-matcher-utils": "29.7.0",
"jest-watch-typeahead": "^2.2.2",
"knip": "^5.10.0",
"lerna": "8.1.7",
"lerna": "8.1.8",
"mini-css-extract-plugin": "2.9.0",
"msw": "2.3.5",
"mutationobserver-shim": "0.3.7",
"ngtemplate-loader": "2.1.0",
"node-notifier": "10.0.1",
"nx": "19.2.0",
"postcss": "8.4.40",
"postcss": "8.4.41",
"postcss-loader": "8.1.1",
"postcss-reporter": "7.1.0",
"postcss-scss": "4.0.9",
@ -215,7 +216,7 @@
"react-test-renderer": "18.2.0",
"redux-mock-store": "1.5.4",
"rimraf": "5.0.7",
"rudder-sdk-js": "2.48.14",
"rudder-sdk-js": "2.48.15",
"sass": "1.77.8",
"sass-loader": "14.2.1",
"smtp-tester": "^2.1.0",
@ -243,7 +244,7 @@
"@emotion/css": "11.11.2",
"@emotion/react": "11.11.4",
"@fingerprintjs/fingerprintjs": "^3.4.2",
"@floating-ui/react": "0.26.20",
"@floating-ui/react": "0.26.21",
"@formatjs/intl-durationformat": "^0.2.4",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/aws-sdk": "0.4.1",
@ -432,7 +433,7 @@
"engines": {
"node": ">= 20"
},
"packageManager": "yarn@4.3.1",
"packageManager": "yarn@4.4.0",
"dependenciesMeta": {
"prettier@3.3.3": {
"unplugged": true

View File

@ -55,14 +55,13 @@ describe('Stats Calculators', () => {
it('should calculate basic stats', () => {
const stats = reduceField({
field: basicTable.fields[0],
reducers: [ReducerID.first, ReducerID.last, ReducerID.mean, ReducerID.count, ReducerID.diffperc],
reducers: [ReducerID.first, ReducerID.last, ReducerID.mean, ReducerID.count],
});
expect(stats.first).toEqual(10);
expect(stats.last).toEqual(20);
expect(stats.mean).toEqual(15);
expect(stats.count).toEqual(2);
expect(stats.diffperc).toEqual(100);
});
it('should handle undefined field data without crashing', () => {

View File

@ -582,7 +582,7 @@ export function doStandardCalcs(field: Field, ignoreNulls: boolean, nullAsZero:
}
if (isNumber(calcs.firstNotNull) && isNumber(calcs.diff)) {
calcs.diffperc = (calcs.diff / calcs.firstNotNull) * 100;
calcs.diffperc = calcs.diff / calcs.firstNotNull;
}
return calcs;
}

View File

@ -20,5 +20,5 @@
"typescript": "5.4.5",
"webpack": "5.91.0"
},
"packageManager": "yarn@4.3.1"
"packageManager": "yarn@4.4.0"
}

View File

@ -37,7 +37,7 @@
},
"dependencies": {
"@emotion/css": "11.11.2",
"@floating-ui/react": "0.26.20",
"@floating-ui/react": "0.26.21",
"@grafana/data": "11.2.0-pre",
"@grafana/experimental": "1.7.13",
"@grafana/faro-web-sdk": "1.9.0",

View File

@ -49,7 +49,7 @@
"dependencies": {
"@emotion/css": "11.11.2",
"@emotion/react": "11.11.4",
"@floating-ui/react": "0.26.20",
"@floating-ui/react": "0.26.21",
"@grafana/data": "11.2.0-pre",
"@grafana/e2e-selectors": "11.2.0-pre",
"@grafana/faro-web-sdk": "^1.3.6",

View File

@ -166,11 +166,14 @@ export const DataLinksInlineEditor = ({
const getDataLinksInlineEditorStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
marginBottom: theme.spacing(2),
display: 'flex',
flexDirection: 'column',
gap: 16,
}),
oneClickOverlay: css({
height: 'auto',
minHeight: 69,
border: `1px dashed ${theme.colors.border.medium}`,
paddingBottom: 10,
fontSize: 10,
color: theme.colors.text.link,
}),

View File

@ -4,7 +4,7 @@ import { Draggable } from '@hello-pangea/dnd';
import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { isCompactUrl } from '../../../utils/dataLinks';
import { isCompactUrl } from '../../../utils';
import { FieldValidationMessage } from '../../Forms/FieldValidationMessage';
import { Icon } from '../../Icon/Icon';
import { IconButton } from '../../IconButton/IconButton';
@ -39,10 +39,6 @@ export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: Da
{...provided.draggableProps}
key={index}
>
<div className={cx(styles.dragHandle, styles.icons)} {...provided.dragHandleProps}>
<Icon name="draggabledots" size="lg" />
</div>
<div className={styles.linkDetails}>
<div className={cx(styles.url, !hasUrl && styles.notConfigured, isCompactExploreUrl && styles.errored)}>
{hasTitle ? title : 'Data link title not provided'}
@ -59,9 +55,12 @@ export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: Da
</FieldValidationMessage>
)}
</div>
<div>
<IconButton name="pen" onClick={onEdit} tooltip="Edit data link title" />
<IconButton name="times" onClick={onRemove} tooltip="Remove data link title" />
<div className={styles.icons}>
<IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit data link" />
<IconButton name="trash-alt" onClick={onRemove} className={styles.icon} tooltip="Remove data link" />
<div className={styles.dragIcon} {...provided.dragHandleProps}>
<Icon name="draggabledots" size="lg" />
</div>
</div>
</div>
</>
@ -78,17 +77,16 @@ const getDataLinkListItemStyles = (theme: GrafanaTheme2) => {
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
marginBottom: theme.spacing(2),
padding: '10px 0 0 10px',
'&:last-child': {
marginBottom: 0,
},
padding: '5px 0 5px 10px',
borderRadius: theme.shape.radius.default,
background: theme.colors.background.secondary,
gap: 8,
}),
linkDetails: css({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
maxWidth: '80%',
maxWidth: `calc(100% - 100px)`,
}),
errored: css({
color: theme.colors.error.text,
@ -118,20 +116,13 @@ const getDataLinkListItemStyles = (theme: GrafanaTheme2) => {
alignItems: 'center',
gap: 8,
}),
dragHandle: css({
dragIcon: css({
cursor: 'grab',
// create focus ring around the whole row when the drag handle is tab-focused
// needs position: relative on the drag row to work correctly
'&:focus-visible&:after': {
bottom: 0,
content: '""',
left: 0,
position: 'absolute',
right: 0,
top: 0,
outline: `2px solid ${theme.colors.primary.main}`,
outlineOffset: '-2px',
},
color: theme.colors.text.secondary,
margin: theme.spacing(0, 0.5),
}),
icon: css({
color: theme.colors.text.secondary,
}),
};
};

View File

@ -10,12 +10,11 @@ import mdx from './Input.mdx';
import { parseAccessory } from './storyUtils';
const prefixSuffixOpts = {
None: null,
Text: '$',
$: 'Text',
...getAvailableIcons().reduce<KeyValue<string>>((prev, c) => {
return {
...prev,
[`Icon: ${c}`]: `icon-${c}`,
[`icon-${c}`]: `Icon: ${c}`,
};
}, {}),
};
@ -43,20 +42,22 @@ const meta: Meta = {
prefixVisible: {
control: {
type: 'select',
options: prefixSuffixOpts,
labels: prefixSuffixOpts,
},
options: [null, ...Object.keys(prefixSuffixOpts)],
},
suffixVisible: {
control: {
type: 'select',
options: prefixSuffixOpts,
labels: prefixSuffixOpts,
},
options: [null, ...Object.keys(prefixSuffixOpts)],
},
type: {
control: {
type: 'select',
options: ['text', 'number', 'password'],
},
options: ['text', 'number', 'password'],
},
// validation: { name: 'Validation regex (will do a partial match if you do not anchor it)' },
width: { control: { type: 'range', min: 10, max: 200, step: 10 } },

View File

@ -12,7 +12,7 @@ export function getPageStyles(theme: GrafanaTheme2) {
? {
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
height: '100vh',
}
: {
display: 'flex',

View File

@ -82,7 +82,7 @@ func dashboardGuardianResponse(err error) response.Response {
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response {
ctx, span := hs.tracer.Start(c.Req.Context(), "httpserver.GetDashboard")
ctx, span := hs.tracer.Start(c.Req.Context(), "api.GetDashboard")
defer span.End()
uid := web.Params(c.Req)[":uid"]
@ -262,6 +262,9 @@ func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string {
}
func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*dashboards.Dashboard, response.Response) {
ctx, span := hs.tracer.Start(ctx, "api.getDashboardHelper")
defer span.End()
var query dashboards.GetDashboardQuery
if len(uid) > 0 {

View File

@ -31,6 +31,9 @@ import (
// Returns a file that is easy to check for changes
// Any changes to the file means we should refresh the frontend
func (hs *HTTPServer) GetFrontendAssets(c *contextmodel.ReqContext) {
c, span := hs.injectSpan(c, "api.GetFrontendAssets")
defer span.End()
hash := sha256.New()
keys := map[string]any{}
@ -97,6 +100,9 @@ func (hs *HTTPServer) GetFrontendSettings(c *contextmodel.ReqContext) {
//
//nolint:gocyclo
func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.FrontendSettingsDTO, error) {
c, span := hs.injectSpan(c, "api.getFrontendSettings")
defer span.End()
availablePlugins, err := hs.availablePlugins(c.Req.Context(), c.SignedInUser.GetOrgID())
if err != nil {
return nil, err
@ -388,6 +394,9 @@ func getShortCommitHash(commitHash string, maxLength int) string {
}
func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlugins AvailablePlugins) (map[string]plugins.DataSourceDTO, error) {
c, span := hs.injectSpan(c, "api.getFSDataSources")
defer span.End()
orgDataSources := make([]*datasources.DataSource, 0)
if c.SignedInUser.GetOrgID() != 0 {
query := datasources.GetDataSourcesQuery{OrgID: c.SignedInUser.GetOrgID(), DataSourceLimit: hs.Cfg.DataSourceLimit}
@ -620,6 +629,9 @@ func (ap AvailablePlugins) Get(pluginType plugins.Type, pluginID string) (*avail
}
func (hs *HTTPServer) availablePlugins(ctx context.Context, orgID int64) (AvailablePlugins, error) {
ctx, span := hs.tracer.Start(ctx, "api.availablePlugins")
defer span.End()
ap := make(AvailablePlugins)
pluginSettingMap, err := hs.pluginSettings(ctx, orgID)
@ -665,6 +677,9 @@ func (hs *HTTPServer) availablePlugins(ctx context.Context, orgID int64) (Availa
}
func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[string]*pluginsettings.InfoDTO, error) {
ctx, span := hs.tracer.Start(ctx, "api.pluginSettings")
defer span.End()
pluginSettings := make(map[string]*pluginsettings.InfoDTO)
// fill settings from database

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social/socialimpl"
"github.com/grafana/grafana/pkg/plugins"
@ -81,6 +82,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
namespacer: request.GetNamespaceMapper(cfg),
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}),
managedPluginsService: managedplugins.NewNoop(),
tracer: tracing.InitializeTracerForTest(),
}
m := web.New()

View File

@ -109,6 +109,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
"github.com/youmark/pkcs8"
)
type HTTPServer struct {
@ -819,6 +820,10 @@ func (hs *HTTPServer) readCertificates() (*tls.Certificate, error) {
return nil, fmt.Errorf(`cannot find SSL key_file at %q`, hs.Cfg.KeyFile)
}
if hs.Cfg.CertPassword != "" {
return handleEncryptedCertificates(hs.Cfg)
}
// previous implementation
tlsCert, err := tls.LoadX509KeyPair(hs.Cfg.CertFile, hs.Cfg.KeyFile)
if err != nil {
return nil, fmt.Errorf("could not load SSL certificate: %w", err)
@ -826,6 +831,50 @@ func (hs *HTTPServer) readCertificates() (*tls.Certificate, error) {
return &tlsCert, nil
}
func handleEncryptedCertificates(cfg *setting.Cfg) (*tls.Certificate, error) {
certKeyFilePassword := cfg.CertPassword
certData, err := os.ReadFile(cfg.CertFile)
if err != nil {
return nil, fmt.Errorf("failed to read certificate file: %w", err)
}
keyData, err := os.ReadFile(cfg.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to read private key file: %w", err)
}
// handle encrypted private key
keyPemBlock, _ := pem.Decode(keyData)
var keyBytes []byte
// Process the PKCS-encrypted PEM block.
if strings.Contains(keyPemBlock.Type, "ENCRYPTED") {
// The pkcs8 package only handles the PKCS #5 v2.0 scheme.
decrypted, err := pkcs8.ParsePKCS8PrivateKey(keyPemBlock.Bytes, []byte(certKeyFilePassword))
if err != nil {
return nil, fmt.Errorf("error parsing PKCS8 Private key: %w", err)
}
keyBytes, err = x509.MarshalPKCS8PrivateKey(decrypted)
if err != nil {
return nil, fmt.Errorf("error marshaling PKCS8 Private key: %w", err)
}
} else {
return nil, fmt.Errorf("password provided but Private key is not encrypted or not supported")
}
var encodedKey bytes.Buffer
err = pem.Encode(&encodedKey, &pem.Block{Type: keyPemBlock.Type, Bytes: keyBytes})
if err != nil {
return nil, fmt.Errorf("error encoding pem file: %w", err)
}
cert, err := tls.X509KeyPair(certData, encodedKey.Bytes())
if err != nil {
return nil, fmt.Errorf("failed to parse X509 key pair: %w", err)
}
return &cert, nil
}
func (hs *HTTPServer) configureTLS() error {
tlsCerts, err := hs.tlsCertificates()
if err != nil {
@ -869,7 +918,7 @@ func (hs *HTTPServer) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, er
return tlsCerts, nil
}
// fsnotify module can be used to detect file changes and based on the event certs can be reloaded
// WatchAndUpdateCerts fsnotify module can be used to detect file changes and based on the event certs can be reloaded
// since it adds a direct dependency for the optional feature. So that is the reason periodic watching
// of cert files is chosen. If fsnotify is added as direct dependency in future, then the implementation
// can be revisited to align to fsnotify.

View File

@ -1,11 +1,12 @@
package api
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHTTPServer_MetricsBasicAuth(t *testing.T) {
@ -37,3 +38,124 @@ func TestHTTPServer_readCertificates(t *testing.T) {
assert.NotNil(t, err)
})
}
func TestHTTPServer_readEncryptedCertificates(t *testing.T) {
t.Run("readCertificates should return certificate if configuration is correct", func(t *testing.T) {
cfg, cleanUpFunc := getHttpServerCfg(t)
defer cleanUpFunc()
ts := &HTTPServer{
Cfg: cfg,
}
c, err := ts.readCertificates()
require.Nil(t, err)
require.NotNil(t, c)
})
t.Run("readCertificates should return error if the password provided is not the correct one", func(t *testing.T) {
cfg, cleanUpFunc := getHttpServerCfg(t)
defer cleanUpFunc()
// change for a wrong password - 32char for consistency
cfg.CertPassword = "somethingThatIsNotTheCorrectPass"
ts := &HTTPServer{
Cfg: cfg,
}
c, err := ts.readCertificates()
require.Nil(t, c)
require.NotNil(t, err)
require.Equal(t, err.Error(), "error parsing PKCS8 Private key: pkcs8: incorrect password")
})
}
// returns Cfg and cleanup function for the created files
func getHttpServerCfg(t *testing.T) (*setting.Cfg, func()) {
// create cert files
cert, err := os.CreateTemp("", "certWithPass*.crt")
require.NoError(t, err)
_, err = cert.Write(certWithPass)
require.NoError(t, err)
privateKey, err := os.CreateTemp("", "privateKey*.key")
require.NoError(t, err)
_, err = privateKey.Write(privateKeyWithPass)
require.NoError(t, err)
cfg := setting.NewCfg()
cfg.CertPassword = password
cfg.CertFile = cert.Name()
cfg.KeyFile = privateKey.Name()
cfg.Protocol = "https"
cleanupFunc := func() {
_ = os.Remove(cert.Name())
_ = os.Remove(privateKey.Name())
}
return cfg, cleanupFunc
}
/*
* Certificates encrypted with password used for testing. These are valid until Aug 1st 2027.
* To generate new ones, use this commands:
*
* # Generate RSA private key with a passphrase '12345678901234567890123456789012'
* sudo openssl genrsa -aes256 -passout pass:12345678901234567890123456789012 -out ./grafana_pass.key 2048
* # Create a new Certificate Signing Request (CSR) using the private key passing passphrase '12345678901234567890123456789012'
* sudo openssl req -new -nodes -sha256 -key ./grafana_pass.key -subj '/CN=testCertWithPass/C=us' -passin pass:12345678901234567890123456789012 -out ./grafana_pass.csr
* # Sign the CSR using the private key to create a self-signed certificate valid for 365 days
* sudo openssl x509 -req -days 1095 -in ./grafana_pass.csr -signkey ./grafana_pass.key -passin pass:12345678901234567890123456789012 -out ./grafana_pass.crt
*/
var certWithPass = []byte(`-----BEGIN CERTIFICATE-----
MIIC1zCCAb8CFGUb9G3+Dl7bTJgCsV0HatdD6jnkMA0GCSqGSIb3DQEBCwUAMCgx
GTAXBgNVBAMMEHRlc3RDZXJ0V2l0aFBhc3MxCzAJBgNVBAYTAnVzMB4XDTI0MDgw
MTE4MzM0OFoXDTI3MDgwMTE4MzM0OFowKDEZMBcGA1UEAwwQdGVzdENlcnRXaXRo
UGFzczELMAkGA1UEBhMCdXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCKnZHWYZLgfpV2MqhTHxpONwQ6dUWWwAl3sQaLV2VH6e0qBhCaO4gCKQbv3KeH
4sXmdYG4fKJ+SnwGhljfW4anQjb/puVSX8E4EXwf81DBUKbUGs5GvIx6oIx2HkoO
BoKBNgsk8K/Eq4XcVUo8PfxbsJzoCyxcrjelV4UDgxpwDCTaewmiIUb+V/JvQi65
J1EWWofghKkNwhZ0Qyh6I9O8N7ZbkEUSbATcZ32AoDhpzhbVXQkNhJJV5SSa2zaA
Bv50cni9Te4PEYq97xUkq2KaD3c+Ie1VrAAmJVCgcUylG1YeZUohyaLbY7DG/PaW
ZPu6OqKddfH1UxUG0xzRjbmJAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEZXVWWV
GdaSUuBlc9Rd6DvSQSBYzBm5zfoQlw1IQT93tI4SVD2U04RPfxUdCh6QxsssitRn
tz2x3EKFBQ3x0jYk+JHxBLdTWAhdWrhFB+beUuOUQ5++cBDTHvpyoROAg/cIz4Fg
PvdhneOlQBe7Vh1Uv4ez+H7U1MtgUAt2LYhb5hundhUpH/WCsn1mlehyhrbDBzPc
f9JeTlZbe6wyvS/26qGPSCgP0KNvltR0Cjf2AV2gjX/7+BUr9qFBRjs4+jZkIRkP
fsYk656OSlFMbYlst1ktnBrmBE7AOHdW/WRynfIFQACNkwnrnPO1u8ZRSUzVlg/2
lzZlmPUgKBVA0kA=
-----END CERTIFICATE-----`)
var privateKeyWithPass = []byte(`-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIpLpJYDO3y4wCAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAx2HkCNR7WRCmF3QiOqhRzBIIE
0O40A8q91zh6j2bseuIMGUQNEeRSf46fUUqtucgV/KAgpQMHL0/tTfhS5GaRcBlm
vry+9Yzfy2So5/SzC6eljdLzOuKHthgn8bBlNb8Z6atmcftr1Geeaw7lXhQqfIj7
qVWQZuU+idSPR3QqKHCpubso4ydyANxDeAuylkdHix9LZFH8oYeJZB48o1adkjVG
nrPuupH/Rm6P7oC8E5x1lMcaAt3DFUaojycXFhGl6vnaejC6oMQqJ58KkHnNrLe+
ltwNCphH35rDGY6mS6a7xMHEfuFHS0bg1Tl5N+vspDg99lFBL92pwdHp8hsoS8Pl
jh4nzsNc0BUQOzDcxh8uHbyAbH8jC7rLs6DUxswSJEE+tDfsKtAu6dcMsbobETTQ
+OIQ0mi2uOQ0G/Fmflf6wPPnWJpWZI/ivHmK4Gmakp+ZSFCyROekO4a5K7J5KbWM
dmv9qFbm0LacQpT/XrS+m1TKNLd1udiJpXULmmWisQTxyorjw84WAvOlaVt1ilSQ
vSYSc1dOvdZO8G0PWa0EoDOIXDohAFeHy+tfBQ/gxSWj2SyC8wpFibchjT9FrMwI
S5NRUmbjHLiIBcHQYhE+ICP238H7v4JaE2LRhljWESRb5eNlD6Ybf0h8WzEjLWmz
RJMNedHnUFV/S1eph3BXUMt+3EKYcAqs+xB80Bi/QgyRBrghlolQS55p3gOyZu8w
NCJ+qsHtFJIaZHDPgD7JOvG8E5Jy8NoFf6qsqROEkVZY3AP9XdK4vx/tn8bSIijX
oTZ04nzud1TKNBaow5/AoyTlPZvToN1IUPXHhpcpvDlz4IvTTL3Owb+//eHphwhS
tbkJyFg7PWQSpL8HcX4zFizmlqhq+hVlPrddlAmR45AL3U10J2TTHyNBo1Lvy9YS
jSe3Ux+gIk30oPRzoVNOXLnACt25LljZ28usuuXTiL2EXL/E7to0z5srOSFpwcZX
0hkokKKqYwjEvGVolfEB9wSxJ9SsapFj+GrEnKdjZacm4rxmzDGaHwKOm/Rbwg2b
XCl3LKFiyJPL0rssMvv6qgelkBzbRwjctXjEa8SIR6s1nOumP2QlYHT1Di66k0+E
zAYm0FNSo2OleRR6pbbXZJXbkUDU931JnON2OPvZ7UhHM2hWfAQq5Nl2KcaqKx/C
eiRV8o8qOuXyNnckWtv7btFj8Y+MLMIt+Ee6ZWeUWQKEFUoGInPUj8KAN8w8K3Z7
BX1JyIJD/qNV9mgKFjmhCI3m2xox5b+RO1NDsDz3S33hsPdBHJHWwBCZLquwq+mM
aSiWiFL8KCK6Fc478J6iUg7Jzd8z3TC02VhCc4p+xWTYEgQN8yUxV2rxSk9mwsWq
v/iOCp07NN9uhNbF4KIrIX010sUYIq8iI1QeiFtQgmooBUHvd3RQH5fLaa5hwozt
hmVfJ7Wl0aBpD516QC09QhQS0jqnFRr433dVRI6zFNdxw3joZPUp4MKBlJ7g0CJV
Iv0fKNJwfT7Vmmwu2M3T5O0NzNx6VkGYXei5+NaJvUwXNwUzmdBUieXyP1bHMhr9
cobRX9pYWflHCH4n0PshBo/quh98Omy7MVcSQtP4S2kQ4uYtZV8pZj1L5K9DekK0
Fx113Ns6T2LzzdARMN7S3qsiRveFRrz+Xm0Rtrl//KB5
-----END ENCRYPTED PRIVATE KEY-----
`)
var password = "12345678901234567890123456789012"

View File

@ -23,6 +23,9 @@ import (
)
func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexViewData, error) {
c, span := hs.injectSpan(c, "api.setIndexViewData")
defer span.End()
settings, err := hs.getFrontendSettings(c)
if err != nil {
return nil, err
@ -215,6 +218,9 @@ func hashUserIdentifier(identifier string, secret string) string {
}
func (hs *HTTPServer) Index(c *contextmodel.ReqContext) {
c, span := hs.injectSpan(c, "api.Index")
defer span.End()
data, err := hs.setIndexViewData(c)
if err != nil {
c.Handle(hs.Cfg, http.StatusInternalServerError, "Failed to get settings", err)

View File

@ -23,6 +23,9 @@ import (
// 422: unprocessableEntityError
// 500: internalServerError
func (hs *HTTPServer) Search(c *contextmodel.ReqContext) response.Response {
c, span := hs.injectSpan(c, "api.Search")
defer span.End()
query := c.Query("query")
tags := c.QueryStrings("tag")
starred := c.Query("starred")

View File

@ -11,6 +11,7 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
"go.opentelemetry.io/otel/trace"
)
func (hs *HTTPServer) GetRedirectURL(c *contextmodel.ReqContext) string {
@ -63,3 +64,9 @@ func ValidateAndNormalizeEmail(email string) (string, error) {
return e.Address, nil
}
func (hs *HTTPServer) injectSpan(c *contextmodel.ReqContext, name string) (*contextmodel.ReqContext, trace.Span) {
ctx, span := hs.tracer.Start(c.Req.Context(), name)
c.Req = c.Req.WithContext(ctx)
return c, span
}

View File

@ -1,6 +1,7 @@
package v0alpha1
import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@ -18,12 +19,13 @@ type ResourceInfo struct {
kind string
newObj func() runtime.Object
newList func() runtime.Object
columns utils.TableColumns
}
func NewResourceInfo(group, version, resourceName, singularName, kind string,
newObj func() runtime.Object, newList func() runtime.Object) ResourceInfo {
newObj func() runtime.Object, newList func() runtime.Object, columns utils.TableColumns) ResourceInfo {
shortName := "" // an optional alias helpful in kubectl eg ("sa" for serviceaccounts)
return ResourceInfo{group, version, resourceName, singularName, shortName, kind, newObj, newList}
return ResourceInfo{group, version, resourceName, singularName, shortName, kind, newObj, newList, columns}
}
func (info *ResourceInfo) WithGroupAndShortName(group string, shortName string) ResourceInfo {
@ -36,6 +38,7 @@ func (info *ResourceInfo) WithGroupAndShortName(group string, shortName string)
shortName: shortName,
newObj: info.newObj,
newList: info.newList,
columns: info.columns,
}
}
@ -113,6 +116,10 @@ func (info *ResourceInfo) NewListFunc() runtime.Object {
return info.newList()
}
func (info *ResourceInfo) TableConverter() utils.TableConvertor {
return utils.NewTableConverter(info.GroupResource(), info.columns)
}
func (info *ResourceInfo) NewNotFound(name string) *errors.StatusError {
return errors.NewNotFound(info.SingularGroupResource(), name)
}

View File

@ -1,10 +1,14 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"fmt"
"time"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
@ -17,18 +21,78 @@ var UserResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"users", "user", "User",
func() runtime.Object { return &User{} },
func() runtime.Object { return &UserList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Login", Type: "string", Format: "string", Description: "The user login"},
{Name: "Email", Type: "string", Format: "string", Description: "The user email"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
u, ok := obj.(*User)
if ok {
return []interface{}{
u.Name,
u.Spec.Login,
u.Spec.Email,
u.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
return nil, fmt.Errorf("expected user")
},
},
)
var TeamResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"teams", "team", "Team",
func() runtime.Object { return &Team{} },
func() runtime.Object { return &TeamList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The team name"},
{Name: "Email", Type: "string", Format: "string", Description: "team email"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*Team)
if !ok {
return nil, fmt.Errorf("expected team")
}
return []interface{}{
m.Name,
m.Spec.Title,
m.Spec.Email,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
},
},
)
var ServiceAccountResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"serviceaccounts", "serviceaccount", "ServiceAccount",
func() runtime.Object { return &ServiceAccount{} },
func() runtime.Object { return &ServiceAccountList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Account", Type: "string", Format: "string", Description: "The service account email"},
{Name: "Email", Type: "string", Format: "string", Description: "The user email"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
u, ok := obj.(*ServiceAccount)
if ok {
return []interface{}{
u.Name,
u.Spec.Name,
u.Spec.Email,
u.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
return nil, fmt.Errorf("expected service account")
},
},
)
var (

View File

@ -11,70 +11,74 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
)
type TableColumns struct {
Definition []metav1.TableColumnDefinition
Reader func(obj any) ([]interface{}, error)
}
type TableConvertor interface {
ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error)
}
// Based on https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/registry/rest/table.go
type customTableConvertor struct {
gr schema.GroupResource
columns []metav1.TableColumnDefinition
reader func(obj any) ([]interface{}, error)
columns TableColumns
}
func NewTableConverter(gr schema.GroupResource, columns []metav1.TableColumnDefinition, reader func(obj any) ([]interface{}, error)) rest.TableConvertor {
converter := customTableConvertor{
gr: gr,
columns: columns,
reader: reader,
func NewTableConverter(gr schema.GroupResource, columns TableColumns) TableConvertor {
if columns.Reader == nil {
columns = TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
v, err := meta.Accessor(obj)
if err == nil && v != nil {
return []interface{}{
v.GetName(),
v.GetCreationTimestamp().UTC().Format(time.RFC3339),
}, nil
}
r := reflect.ValueOf(obj).Elem()
n := r.FieldByName("Name").String()
if n != "" {
return []interface{}{
n,
"",
}, nil
}
return []interface{}{
fmt.Sprintf("%v", obj),
"",
}, nil
},
}
}
// Replace the description on standard columns with the global values
for idx, column := range converter.columns {
for idx, column := range columns.Definition {
if column.Description == "" {
switch column.Name {
case "Name":
converter.columns[idx].Description = swaggerMetadataDescriptions["name"]
columns.Definition[idx].Description = swaggerMetadataDescriptions["name"]
case "Created At":
converter.columns[idx].Description = swaggerMetadataDescriptions["creationTimestamp"]
columns.Definition[idx].Description = swaggerMetadataDescriptions["creationTimestamp"]
}
}
}
return converter
return customTableConvertor{
gr: gr,
columns: columns,
}
}
func NewDefaultTableConverter(gr schema.GroupResource) rest.TableConvertor {
return NewTableConverter(gr,
[]metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Created At", Type: "date"},
},
func(obj any) ([]interface{}, error) {
v, err := meta.Accessor(obj)
if err == nil && v != nil {
return []interface{}{
v.GetName(),
v.GetCreationTimestamp().UTC().Format(time.RFC3339),
}, nil
}
r := reflect.ValueOf(obj).Elem()
n := r.FieldByName("Name").String()
if n != "" {
return []interface{}{
n,
"",
}, nil
}
return []interface{}{
fmt.Sprintf("%v", obj),
"",
}, nil
},
)
}
var _ rest.TableConvertor = &customTableConvertor{}
var _ TableConvertor = &customTableConvertor{}
var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()
func (c customTableConvertor) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
@ -85,12 +89,9 @@ func (c customTableConvertor) ConvertToTable(ctx context.Context, object runtime
table = &metav1.Table{}
}
fn := func(obj runtime.Object) error {
cells, err := c.reader(obj)
cells, err := c.columns.Reader(obj)
if err != nil {
resource := c.gr
if info, ok := request.RequestInfoFrom(ctx); ok {
resource = schema.GroupResource{Group: info.APIGroup, Resource: info.Resource}
}
return errNotAcceptable{resource: resource}
}
table.Rows = append(table.Rows, metav1.TableRow{
@ -119,7 +120,7 @@ func (c customTableConvertor) ConvertToTable(ctx context.Context, object runtime
}
}
if opt, ok := tableOptions.(*metav1.TableOptions); !ok || !opt.NoHeaders {
table.ColumnDefinitions = c.columns
table.ColumnDefinitions = c.columns.Definition
}
return table, nil
}

View File

@ -7,33 +7,34 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
)
func TestTableConverter(t *testing.T) {
// dummy converter
converter := utils.NewTableConverter(
schema.GroupResource{Group: "x", Resource: "y"},
[]metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Dummy", Type: "string", Format: "string", Description: "Something here"},
{Name: "Created At", Type: "date"},
},
func(obj any) ([]interface{}, error) {
m, ok := obj.(*metav1.APIGroup)
if !ok {
return nil, fmt.Errorf("expected status")
}
ts := metav1.NewTime(time.UnixMilli(10000000))
return []interface{}{
m.Name,
"dummy",
ts.Time.UTC().Format(time.RFC3339),
}, nil
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Dummy", Type: "string", Format: "string", Description: "Something here"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*metav1.APIGroup)
if !ok {
return nil, fmt.Errorf("expected status")
}
ts := metav1.NewTime(time.UnixMilli(10000000))
return []interface{}{
m.Name,
"dummy",
ts.Time.UTC().Format(time.RFC3339),
}, nil
},
},
)
@ -97,7 +98,7 @@ func TestTableConverter(t *testing.T) {
// Default table converter
// Convert a single table
converter = utils.NewDefaultTableConverter(schema.GroupResource{Group: "x", Resource: "y"})
converter = utils.NewTableConverter(schema.GroupResource{Group: "x", Resource: "y"}, utils.TableColumns{})
table, err = converter.ConvertToTable(context.Background(), &metav1.APIGroup{
Name: "hello",
}, nil)

View File

@ -1,17 +1,18 @@
package v0alpha1
import "encoding/json"
import (
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
// Integration defines model for Integration.
// +k8s:openapi-gen=true
type Integration struct {
DisableResolveMessage *bool `json:"disableResolveMessage,omitempty"`
// +mapType=atomic
SecureFields map[string]bool `json:"SecureFields,omitempty"`
// +listType=atomic
Settings json.RawMessage `json:"settings"`
Type string `json:"type"`
Uid *string `json:"uid,omitempty"`
SecureFields map[string]bool `json:"secureFields,omitempty"`
Settings common.Unstructured `json:"settings"`
Type string `json:"type"`
Uid *string `json:"uid,omitempty"`
}
// ReceiverSpec defines model for Spec.

View File

@ -10,6 +10,7 @@ import (
"k8s.io/apiserver/pkg/registry/generic"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
scope "github.com/grafana/grafana/pkg/apis/scope/v0alpha1"
)
@ -28,11 +29,44 @@ var (
"timeintervals", "timeinterval", "TimeInterval",
func() runtime.Object { return &TimeInterval{} },
func() runtime.Object { return &TimeIntervalList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
// {Name: "Intervals", Type: "string", Format: "string", Description: "The display name"},
},
Reader: func(obj any) ([]interface{}, error) {
r, ok := obj.(*TimeInterval)
if ok {
return []interface{}{
r.Name,
// r.Spec, //TODO implement formatting for Spec, same as UI?
}, nil
}
return nil, fmt.Errorf("expected resource or info")
},
},
)
ReceiverResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"receivers", "receiver", "Receiver",
func() runtime.Object { return &Receiver{} },
func() runtime.Object { return &ReceiverList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The receiver name"}, // TODO: Add integration types.
},
Reader: func(obj any) ([]interface{}, error) {
r, ok := obj.(*Receiver)
if ok {
return []interface{}{
r.Name,
r.Spec.Title,
// r.Spec, //TODO implement formatting for Spec, same as UI?
}, nil
}
return nil, fmt.Errorf("expected resource or info")
},
},
)
// SchemeGroupVersion is group version used to register these objects
SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION}

View File

@ -8,8 +8,6 @@
package v0alpha1
import (
json "encoding/json"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@ -28,11 +26,7 @@ func (in *Integration) DeepCopyInto(out *Integration) {
(*out)[key] = val
}
}
if in.Settings != nil {
in, out := &in.Settings, &out.Settings
*out = make(json.RawMessage, len(*in))
copy(*out, *in)
}
in.Settings.DeepCopyInto(&out.Settings)
if in.Uid != nil {
in, out := &in.Uid, &out.Uid
*out = new(string)

View File

@ -48,28 +48,12 @@ func schema_pkg_apis_alerting_notifications_v0alpha1_Integration(ref common.Refe
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: false,
Type: []string{"boolean"},
Format: "",
},
},
},
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
"settings": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "byte",
Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"),
},
},
"type": {
@ -89,6 +73,8 @@ func schema_pkg_apis_alerting_notifications_v0alpha1_Integration(ref common.Refe
Required: []string{"settings", "type"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"},
}
}

View File

@ -1,10 +1,14 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"fmt"
"time"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
@ -17,6 +21,26 @@ var DashboardResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"dashboards", "dashboard", "Dashboard",
func() runtime.Object { return &Dashboard{} },
func() runtime.Object { return &DashboardList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The dashboard name"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
dash, ok := obj.(*Dashboard)
if ok {
if dash != nil {
return []interface{}{
dash.Name,
dash.Spec.GetNestedString("title"),
dash.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
}
return nil, fmt.Errorf("expected dashboard or summary")
},
},
)
var (

View File

@ -1,10 +1,14 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"fmt"
"time"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
@ -17,6 +21,24 @@ var DashboardSnapshotResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"dashboardsnapshots", "dashboardsnapshot", "DashboardSnapshot",
func() runtime.Object { return &DashboardSnapshot{} },
func() runtime.Object { return &DashboardSnapshotList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The snapshot name"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*DashboardSnapshot)
if ok {
return []interface{}{
m.Name,
m.Spec.Title,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
}
return nil, fmt.Errorf("expected snapshot")
},
}, // default table converter
)
var (

View File

@ -1,9 +1,13 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"fmt"
"time"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
const (
@ -15,4 +19,24 @@ var GenericConnectionResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"connections", "connection", "DataSourceConnection",
func() runtime.Object { return &DataSourceConnection{} },
func() runtime.Object { return &DataSourceConnectionList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The datasource title"},
{Name: "APIVersion", Type: "string", Format: "string", Description: "API Version"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*DataSourceConnection)
if !ok {
return nil, fmt.Errorf("expected connection")
}
return []interface{}{
m.Name,
m.Title,
m.APIVersion,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
},
},
)

View File

@ -1,10 +1,13 @@
package v0alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"fmt"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
@ -18,6 +21,24 @@ var FeatureResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"features", "feature", "Feature",
func() runtime.Object { return &Feature{} },
func() runtime.Object { return &FeatureList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Stage", Type: "string", Format: "string", Description: "Where is the flag in the dev cycle"},
{Name: "Owner", Type: "string", Format: "string", Description: "Which team owns the feature"},
},
Reader: func(obj any) ([]interface{}, error) {
r, ok := obj.(*Feature)
if ok {
return []interface{}{
r.Name,
r.Spec.Stage,
r.Spec.Owner,
}, nil
}
return nil, fmt.Errorf("expected resource or info")
},
},
)
// TogglesResourceInfo represents the actual configuration
@ -25,6 +46,7 @@ var TogglesResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"featuretoggles", "featuretoggle", "FeatureToggles",
func() runtime.Object { return &FeatureToggles{} },
func() runtime.Object { return &FeatureTogglesList{} },
utils.TableColumns{}, // default table converter
)
var (

View File

@ -1,10 +1,13 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"fmt"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
@ -18,6 +21,25 @@ var FolderResourceInfo = common.NewResourceInfo(GROUP, VERSION,
RESOURCE, "folder", "Folder",
func() runtime.Object { return &Folder{} },
func() runtime.Object { return &FolderList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The display name"},
{Name: "Parent", Type: "string", Format: "string", Description: "Parent folder UID"},
},
Reader: func(obj any) ([]interface{}, error) {
r, ok := obj.(*Folder)
if ok {
accessor, _ := utils.MetaAccessor(r)
return []interface{}{
r.Name,
r.Spec.Title,
accessor.GetFolder(),
}, nil
}
return nil, fmt.Errorf("expected folder")
},
},
)
var (

View File

@ -6,6 +6,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
const (
@ -18,6 +19,7 @@ var QueryTemplateResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"querytemplates", "querytemplate", "QueryTemplate",
func() runtime.Object { return &QueryTemplate{} },
func() runtime.Object { return &QueryTemplateList{} },
utils.TableColumns{}, // default table converter
)
var (

View File

@ -1,10 +1,14 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"fmt"
"time"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
@ -19,6 +23,26 @@ var PlaylistResourceInfo = common.NewResourceInfo(GROUP, VERSION,
RESOURCE, "playlist", "Playlist",
func() runtime.Object { return &Playlist{} },
func() runtime.Object { return &PlaylistList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The playlist name"},
{Name: "Interval", Type: "string", Format: "string", Description: "How often the playlist will update"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*Playlist)
if !ok {
return nil, fmt.Errorf("expected playlist")
}
return []interface{}{
m.Name,
m.Spec.Title,
m.Spec.Interval,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
},
},
)
var (

View File

@ -5,6 +5,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
)
const (
@ -17,12 +18,14 @@ var DataSourceApiServerResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"datasourceapiservers", "datasourceapiserver", "DataSourceApiServer",
func() runtime.Object { return &DataSourceApiServer{} },
func() runtime.Object { return &DataSourceApiServerList{} },
utils.TableColumns{}, // default table converter
)
var QueryTypeDefinitionResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"querytypes", "querytype", "QueryTypeDefinition",
func() runtime.Object { return &QueryTypeDefinition{} },
func() runtime.Object { return &QueryTypeDefinitionList{} },
utils.TableColumns{}, // default table converter
)
var (

View File

@ -1,10 +1,14 @@
package v0alpha1
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"fmt"
"time"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
@ -17,18 +21,84 @@ var ScopeResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"scopes", "scope", "Scope",
func() runtime.Object { return &Scope{} },
func() runtime.Object { return &ScopeList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Created At", Type: "date"},
{Name: "Title", Type: "string"},
{Name: "Filters", Type: "array"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*Scope)
if !ok {
return nil, fmt.Errorf("expected scope")
}
return []interface{}{
m.Name,
m.CreationTimestamp.UTC().Format(time.RFC3339),
m.Spec.Title,
m.Spec.Filters,
}, nil
},
}, // default table converter
)
var ScopeDashboardBindingResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"scopedashboardbindings", "scopedashboardbinding", "ScopeDashboardBinding",
func() runtime.Object { return &ScopeDashboardBinding{} },
func() runtime.Object { return &ScopeDashboardBindingList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Created At", Type: "date"},
{Name: "Dashboard", Type: "string"},
{Name: "Scope", Type: "string"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*ScopeDashboardBinding)
if !ok {
return nil, fmt.Errorf("expected scope dashboard binding")
}
return []interface{}{
m.Name,
m.CreationTimestamp.UTC().Format(time.RFC3339),
m.Spec.Dashboard,
m.Spec.Scope,
}, nil
},
},
)
var ScopeNodeResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"scopenodes", "scopenode", "ScopeNode",
func() runtime.Object { return &ScopeNode{} },
func() runtime.Object { return &ScopeNodeList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Created At", Type: "date"},
{Name: "Title", Type: "string"},
{Name: "Parent Name", Type: "string"},
{Name: "Node Type", Type: "string"},
{Name: "Link Type", Type: "string"},
{Name: "Link ID", Type: "string"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*ScopeNode)
if !ok {
return nil, fmt.Errorf("expected scope node")
}
return []interface{}{
m.Name,
m.CreationTimestamp.UTC().Format(time.RFC3339),
m.Spec.Title,
m.Spec.ParentName,
m.Spec.NodeType,
m.Spec.LinkType,
m.Spec.LinkID,
}, nil
},
}, // default table converter
)
var (

View File

@ -1,11 +1,14 @@
package v0alpha1
import (
"fmt"
"time"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
)
const (
@ -18,6 +21,24 @@ var ExternalNameResourceInfo = common.NewResourceInfo(GROUP, VERSION,
"externalnames", "externalname", "ExternalName",
func() runtime.Object { return &ExternalName{} },
func() runtime.Object { return &ExternalNameList{} },
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Host", Type: "string", Format: "string", Description: "The service host"},
{Name: "Created At", Type: "date"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*ExternalName)
if !ok {
return nil, fmt.Errorf("expected external name")
}
return []interface{}{
m.Name,
m.Spec.Host,
m.CreationTimestamp.UTC().Format(time.RFC3339),
}, nil
},
}, // default table converter
)
var (

View File

@ -117,8 +117,13 @@ func doInstallPlugin(ctx context.Context, pluginID, version string, o pluginInst
// If a version is specified, check if it is already installed
if version != "" {
if services.PluginVersionInstalled(pluginID, version, o.pluginDir) {
if p, ok := services.PluginVersionInstalled(pluginID, version, o.pluginDir); ok {
services.Logger.Successf("Plugin %s v%s already installed.", pluginID, version)
for _, depP := range p.JSONData.Dependencies.Plugins {
if err := doInstallPlugin(ctx, depP.ID, depP.Version, o, installing); err != nil {
return err
}
}
return nil
}
}
@ -133,13 +138,28 @@ func doInstallPlugin(ctx context.Context, pluginID, version string, o pluginInst
var archive *repo.PluginArchive
var err error
pluginZipURL := o.pluginURL
if pluginZipURL != "" {
if archive, err = repository.GetPluginArchiveByURL(ctx, pluginZipURL, compatOpts); err != nil {
if o.pluginURL != "" {
archive, err = repository.GetPluginArchiveByURL(ctx, o.pluginURL, compatOpts)
if err != nil {
return err
}
} else {
if archive, err = repository.GetPluginArchive(ctx, pluginID, version, compatOpts); err != nil {
archiveInfo, err := repository.GetPluginArchiveInfo(ctx, pluginID, version, compatOpts)
if err != nil {
return err
}
if p, ok := services.PluginVersionInstalled(pluginID, archiveInfo.Version, o.pluginDir); ok {
services.Logger.Successf("Plugin %s v%s already installed.", pluginID, archiveInfo.Version)
for _, depP := range p.JSONData.Dependencies.Plugins {
if err = doInstallPlugin(ctx, depP.ID, depP.Version, o, installing); err != nil {
return err
}
}
return nil
}
if archive, err = repository.GetPluginArchiveByURL(ctx, archiveInfo.URL, compatOpts); err != nil {
return err
}
}

View File

@ -88,14 +88,14 @@ func GetLocalPlugins(pluginDir string) []*plugins.FoundBundle {
return res
}
func PluginVersionInstalled(pluginID, version, pluginDir string) bool {
func PluginVersionInstalled(pluginID, version, pluginDir string) (plugins.FoundPlugin, bool) {
for _, bundle := range GetLocalPlugins(pluginDir) {
pJSON := bundle.Primary.JSONData
if pJSON.ID == pluginID {
if pJSON.Info.Version == version {
return true
return bundle.Primary, true
}
}
}
return false
return plugins.FoundPlugin{}, false
}

View File

@ -0,0 +1,6 @@
# apiserver-certificates
These certificates are used for development and testing ONLY. They are generated using the script under
[hack/make-aggregator-pki.sh](./hack/make-aggregator-pki.sh).
The CA, server and client certificates are each 10 years of expiration.

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID9zCCAt+gAwIBAgIUeRrA5l+Rl4LkHPP1DmMFlYzrhW4wDQYJKoZIhvcNAQEL
BQAwgYoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApOZXcgU3dlZGVuMRMwEQYDVQQH
DApTdG9ja2hvbG0gMRAwDgYDVQQKDAdHcmFmYW5hMQwwCgYDVQQLDANSJkQxEDAO
BgNVBAMMB3Rlc3QtY2ExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZ3JhZmFuYS5hcHAw
HhcNMjQwODA1MjMxMzUzWhcNMzQwODAzMjMxMzUzWjCBijELMAkGA1UEBhMCVVMx
EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV
BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G
CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBANj6qsutYwof0e0zHrp35Dey+kQxi+VTr/sAKlSoyySP4fmQ
9Qn8mDY4HyJ1oOJFpFAlD0Qp1xGdbvcrlvjoieqmfenW342fza0wqS5K8qkd2rJ7
khdAE2mACZTFSjmAa8+1rIRWnR0SaHBmDgdxBfNkET+n+cX+WsDMhmzNvPoPDS/V
8LaNih/eOUzb/5hamvD8CNLKakes0u/EsdxOsGFWCkpE1mg9yg0YPms5qUAj9pdV
iPH8B5zA1JoukZCrVGPv6R76fJI1LEohiASNFt9cgs2dhdk6QHzGyqNq3T3Cw8yI
Cug/Kk9DGqwq9OeXtADa4hhPebj04C4hxk0AT2UCAwEAAaNTMFEwHQYDVR0OBBYE
FBBn2SXiiItJQsJZ7MTvIn1s1t3jMB8GA1UdIwQYMBaAFBBn2SXiiItJQsJZ7MTv
In1s1t3jMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABcCcwsg
jrcNZheAaKXtdcZT01+doQhaOvhZQhY5L+bCnB0wkBXRJzv8ne+KgLx7auFQP8/2
OoPgaiA0R+XtImCkyBd+cr4mo2tYVpHt9+B0HaYGzGoXt0dre47ihlgkqoSwmgvG
9++pfrbQGRd5Xb/j0468sd5uQy1PPhsjCzFZTuxXcaAN13MDNikYjjn5mc5coklu
hCFH54PgP/PUDXxI0v/QUjNOj7hAdMkqOjzFD9Fze1KjtS3aSZvaaZVrM3x/YS8y
1IUgyocgoOKCqBOeEict+g/xghFDe7r2Dlgps/hPD1ojijBl83g5i079jW4y9jm+
osFOTGnRx2u0CpE=
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDY+qrLrWMKH9Ht
Mx66d+Q3svpEMYvlU6/7ACpUqMskj+H5kPUJ/Jg2OB8idaDiRaRQJQ9EKdcRnW73
K5b46Inqpn3p1t+Nn82tMKkuSvKpHdqye5IXQBNpgAmUxUo5gGvPtayEVp0dEmhw
Zg4HcQXzZBE/p/nF/lrAzIZszbz6Dw0v1fC2jYof3jlM2/+YWprw/AjSympHrNLv
xLHcTrBhVgpKRNZoPcoNGD5rOalAI/aXVYjx/AecwNSaLpGQq1Rj7+ke+nySNSxK
IYgEjRbfXILNnYXZOkB8xsqjat09wsPMiAroPypPQxqsKvTnl7QA2uIYT3m49OAu
IcZNAE9lAgMBAAECggEAYUTyNy+J1BqSrd66WkZv6SZTeimp+Mrs+71FvMEUnFXi
LFJ2/xydEcVT88s+reEheYo7j0egcfWdLrH8UqZQWYB8ts0MV715YzgKx8Vyhizr
gxLRWZnwed2brfVJwoBXFHzxkzwO398GMckWZfCdhdBoyRwg5UkS3xZw9qq+mmw8
hvt88sDzHwnk/9rMY6KLNhWNiCSAxU65AlpktWRGy2e9wAtyzN+WX/iQwsiAyhk2
TV2bHQxRE7FT9hg4UBxzWruYRj4jkLKdH4tVqVKLsV7KutAOiYj3hCjYzOac/5QG
XrTRhN/ewAqJCTOI9K8BVT27l3QxREcq480auDwIawKBgQDzOr6+ILgPdqdGX9Nu
HoLnNuU6OdFsTJ3qXSp4AGv8ufq47PJmTp3F2ZN9RTDr6Fih7UKhApVvFOy6/Hlr
vb0MbL9FOe+2ejtnVmNi13AMexW+nmnOs1oSghXYIbt51/n+5bFmzAo6RFDEG0YT
mtRWMj4mifMI0XH31yRk+Yjp6wKBgQDkXxdG0OmoDwlrlbHjVOpEg7/A0diBHfb9
yh4MbaVmd6rGgdNkJYq1qB6ctJcLYpKq1QGgj6FyNDNyY/T4lIxRuSHqC2v1Mjt/
TaL1BhMsH2Q6+bT8mlmM4cNNgSVHfISJv2mMuKfEqb+uY0y8UU6/lYmwzC5rH4bs
TgPNSLmH7wKBgQCk8F9M2y81/UZt6Kmd8T7fwFAt7etgP4yO02LrQY35Mb0eDkBK
tGE1O9hSiMsmDseb9yLJwNDJJS1rl65XK7G5bT0/mow9+CG0b9axvlqTfBxAyXgC
3YjlKCXcDPPvKlCzU9u7U/5TiOQkOEKLJOF9GlEfHUkb37wjT1e0yarYxQKBgFNF
nT40RU8DlKLHJeNH/lhXVh9gJTsHix2Fiqlrfck8T2gsxMEas1aD5A2uB/mdyu9B
1mMOnIcBI9VNP3E48WWHRSeLXKU+2NUVoRsJSQpos+qRTP5i5c5qMAXd1pMXg1ib
FEi8uGgMoZlcGgn89+MCCwANo8tp5o/Z7qb3Ire/AoGANmc8BAGgKanJVAfu7gDU
AgxRg5T0/C6vpWh5k/gFS7AQqgnx8EcRYtuCD4Er5GFRdcyb9AuZccyKzWI5JS89
YOmJPrfDd7VzBsrOjwJCXcV6tUUq6yO4Ra8vKlIw7T+mMYZh7xWwGnT8UA+X9F+7
yV6kPpraMD/dFbo2TZoNMjc=
-----END PRIVATE KEY-----

View File

@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEjDCCA3SgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBijELMAkGA1UEBhMCVVMx
EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV
BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G
CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDAeFw0yNDA4MDUyMzEzNTRaFw0z
NDA4MDMyMzEzNTRaMC8xFDASBgNVBAMMC2RldmVsb3BtZW50MRcwFQYDVQQKDA5z
eXN0ZW06bWFzdGVyczCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMz3
SSM62uPCNG7mbdlgE0bYcpT8f1ZFmyaauXzbqdTF0/VxGCh9YVBFnqlAdfAUFasN
HBLQz63PniOWbzmhXZBkrCF/nozg7PgiXJ7aM5ppJyYulOI3sldH49V8KWzcGiJo
ZE13kKIlCqnOanErj4RnaxVJvMmWOlt6Zn/ljkDFvwypaOeIX/YO1zuZ95BqGpDh
u4CXipKeCM0AjXNvKrHCi5QbNEF0FMyvsX+s76wZtZSpB7vbIfj/h5Z4Scfyovmy
cZeqgL2mDAnosKw8/KYAgkcArMHoScTfdCjWthJQlPybJKgMKOJI9OehuP1a89A9
t+T/T8ZT75x0YADw+56WDcsYGECiaixrkpB0aiPBtgDm1i1z7ooqSfAfZa0PEgPT
L680TK66qXI2GFtNqUkU6+xZEfvuxOdZRXkuIVeQ4UMqaiW9y37ppvKzX4llOD2D
VP86D0JxCCuJqxlzSAGbxK61c8mdqM1SKAa5O5Kl0KHxVkGk4zHYtrHqWsy6YOIM
CqLMA2vLAeqYwSEn2eQBkTNdhSulrB6JPNBDPNB9+wX5EOX3w+x9lRxn59kQgKyV
anf1cm1UYeqjsLPhBjpuwVtHhEAc8uJrSZBM3oKPSYEYxwEo+QG8rcKIpjV9gtCu
T3e/WBWlDVBm0NwbxtY/rYc5ggPHMtPhFxBDhjJpAgMBAAGjVzBVMBMGA1UdJQQM
MAoGCCsGAQUFBwMCMB0GA1UdDgQWBBQ+z7tF4/VANXrXgsY0PVQFFo6csjAfBgNV
HSMEGDAWgBQQZ9kl4oiLSULCWezE7yJ9bNbd4zANBgkqhkiG9w0BAQsFAAOCAQEA
NagU3hqJr5wEU79202Rj+aqbzWJlz1jvZVR6PHILB0deTtvYk1EXeVgyjwmz7PW3
DLYcpwgW3bTdxhejduFNzKazDOZZ2blUlZlHs1PoBb/ipnw4g+ozO+ZyGs05gCu8
DrsxUKX7bpVcKGfPNVg8L4xZbanizO1XUiv6PDBBRZhKXSl+KO9+aN2C/yRnYmpV
9dyuMI9nFoMB0K7rxTxiRCIPIWs8nsGouLa6lg6/I+xTAjV0IqNz0rQ66UWJOr3x
vLGFdMMaDUSbsNlu18/sCJd+G0rkh24YE6e3I1wGqE9jr4iYsMhkhfp0u4Qojmfp
3/7IGZYzvVIN/PvSFBBgZw==
-----END CERTIFICATE-----

View File

@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEmjCCAoICAQAwLzEUMBIGA1UEAwwLZGV2ZWxvcG1lbnQxFzAVBgNVBAoMDnN5
c3RlbTptYXN0ZXJzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzPdJ
Izra48I0buZt2WATRthylPx/VkWbJpq5fNup1MXT9XEYKH1hUEWeqUB18BQVqw0c
EtDPrc+eI5ZvOaFdkGSsIX+ejODs+CJcntozmmknJi6U4jeyV0fj1XwpbNwaImhk
TXeQoiUKqc5qcSuPhGdrFUm8yZY6W3pmf+WOQMW/DKlo54hf9g7XO5n3kGoakOG7
gJeKkp4IzQCNc28qscKLlBs0QXQUzK+xf6zvrBm1lKkHu9sh+P+HlnhJx/Ki+bJx
l6qAvaYMCeiwrDz8pgCCRwCswehJxN90KNa2ElCU/JskqAwo4kj056G4/Vrz0D23
5P9PxlPvnHRgAPD7npYNyxgYQKJqLGuSkHRqI8G2AObWLXPuiipJ8B9lrQ8SA9Mv
rzRMrrqpcjYYW02pSRTr7FkR++7E51lFeS4hV5DhQypqJb3Lfumm8rNfiWU4PYNU
/zoPQnEIK4mrGXNIAZvErrVzyZ2ozVIoBrk7kqXQofFWQaTjMdi2sepazLpg4gwK
oswDa8sB6pjBISfZ5AGRM12FK6WsHok80EM80H37BfkQ5ffD7H2VHGfn2RCArJVq
d/VybVRh6qOws+EGOm7BW0eEQBzy4mtJkEzego9JgRjHASj5AbytwoimNX2C0K5P
d79YFaUNUGbQ3BvG1j+thzmCA8cy0+EXEEOGMmkCAwEAAaAmMCQGCSqGSIb3DQEJ
DjEXMBUwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggIBAB7x
82Hy942/7nSbz9gJUsJ70sYcre0VRduIMzP1Ytk/ni+1QXDZSmdSrgR0cbTL4VIZ
zSt5Bp2vxBJqHmGPOXv5oWI+nGrfWzmzBGoGNk4cdNW2ZZCn6Om4L16gLYIlxmm1
vgCgnPOYMEi5qtgG6676wSX7gf5+j1gWVDEwNFil8wBUIlA9QRbc2NN0T6479Z4b
4tUHzUpGuf788OPUqAdxpkH1xZBKW8HEGsk1+NdHgb5krE2ElwvN9qWX5f2DzzwL
sQf3A2IrO4lrso4RHao4X3V+/DOZCx+FK/oXqzyoS4YNnNy6B0377LgYkuuJsbOo
IFC7vRhBORvAXEXD/scLEsGKuQzs1vLLiwOp7pBOcGZzxOoqoXc2A6h64qpZKriq
T1rK6rmrttaXqYUokjbg/ggpcXCC1BGxsoRvGQoN6aQNV27zUh2wpZqpCpVLoN21
eCqM9LVoPMCRqn7ItXE9oJhasPlKDO6amHL3CtxjvU2meXoa+nCia8lL4Sb9pSkB
O5eX4k3H/m4zjrpbqp7UdnidcYn4zfrjRBr65bqNr1sOxwUDWOa0kqaOKrgqwhWn
ld6zkuekuwzrOK4+Dpf5ybWnVFi8WSz6k9TqQIaeMrzeCBVboufl/ygoIRsTqp7N
WcKuZdUx8t16hFZ/NgpHRgfZI9pYdzi2vNSZ6WMM
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDM90kjOtrjwjRu
5m3ZYBNG2HKU/H9WRZsmmrl826nUxdP1cRgofWFQRZ6pQHXwFBWrDRwS0M+tz54j
lm85oV2QZKwhf56M4Oz4Ilye2jOaaScmLpTiN7JXR+PVfCls3BoiaGRNd5CiJQqp
zmpxK4+EZ2sVSbzJljpbemZ/5Y5Axb8MqWjniF/2Dtc7mfeQahqQ4buAl4qSngjN
AI1zbyqxwouUGzRBdBTMr7F/rO+sGbWUqQe72yH4/4eWeEnH8qL5snGXqoC9pgwJ
6LCsPPymAIJHAKzB6EnE33Qo1rYSUJT8mySoDCjiSPTnobj9WvPQPbfk/0/GU++c
dGAA8Puelg3LGBhAomosa5KQdGojwbYA5tYtc+6KKknwH2WtDxID0y+vNEyuuqly
NhhbTalJFOvsWRH77sTnWUV5LiFXkOFDKmolvct+6abys1+JZTg9g1T/Og9CcQgr
iasZc0gBm8SutXPJnajNUigGuTuSpdCh8VZBpOMx2Lax6lrMumDiDAqizANrywHq
mMEhJ9nkAZEzXYUrpaweiTzQQzzQffsF+RDl98PsfZUcZ+fZEICslWp39XJtVGHq
o7Cz4QY6bsFbR4RAHPLia0mQTN6Cj0mBGMcBKPkBvK3CiKY1fYLQrk93v1gVpQ1Q
ZtDcG8bWP62HOYIDxzLT4RcQQ4YyaQIDAQABAoICAAP5CpPqMauJZv9bzCGy9aix
bGt5ce/pMTrNiV5zel5yDQZ/FXpqb2WRNeQ0CEfZzHo4arStfgsGy6WcW7tMnAie
6kG2IdZQoUiQJPFcy3QuSS3u/Z/dYmxD69VRCZNZ4vqgq/S0RmDIMkWB515acMGT
82zHapH8jWoawcwalgK+JSf7YXnJvZ9tC0xf0tSRI+2ZPIDKHyuHReZ7ALg4iEWy
CUclgwJTm/gZiIman497dOwviGN3zxf5aaZtCN69Pp4I+1/sKYA+N3yag/smAvkm
PcrfHITKf3c3ZBQLEQl4Qk0GUPlzY+L6n57yQOijQnd6Yhs9kiKnBkLRyXnB3K9T
M/ZDp7BHSO+e+qENsYk7POzgeU8udF3mS/h17/7kXIdvi/7l/6Dh2YlEqjH+p3XX
AKLKwx01c++h9aa9RXU+PpwyZoKpYhe16kMceF/bxbr74w5Ukqzny5iEX1t0oCfn
KncheoJQiIcC2O1pZLGRzxSlizvbReP9fGPV9qFmDMvdawK14472hT+mNZw4fziq
dLGr9MZ3hVThwIl1ylA9qCJQ0VbaD7Z6YIx0fO2owro4M6Xv247/KZQ7hM1rUo3F
crqe82qysOrAysgDjvho4iCPKW9d/kUPqYA7DHUOvHBkvsgUoc2TNmV48moYOaox
BoHBtDN49SyQpHaoG+NxAoIBAQD2dHhiFyQlTUeym6jpc3l0tLt1Y6OTwkNi0vQo
7j+aNSGSPkcQkhObM54G1pE+DDy2pkpS8m2teKGmnXMBK+TLS/WjcuJbLvGAxlY7
mRzJh1bzL+1aEa8ZOCtc3FcVBdGTvwCInEzx2r5ghWEgBvy6SQcVs76ALfteUDvI
HxZG1Xou3Il3O+vaxK8ure2SJJk710NQfSF0qaUR6qpLnL3zsRS9GXQ1V/6Ek8CQ
0tdMME+reTC7wh51s/oCNh3QoJThsv4DQQD69SBX8kfy1+qaLOxbNAefrTQk6Pd2
xNCu4szAG7QyiF/5jOzV5JJYVTnrWqsa5obzCcIbjZWTEPSRAoIBAQDU53bHHDSA
S0oGRpxf9gzz2xVqbG+slraCs9pNA9fkcQostATP9pWvnrch40yOTMyXi2ldVBW8
ZO8R7jVVA8mbSIyPv9a5V/AuqpRX1fFgojaFY3mEw52CYpmg6/thCESBb6ILXB4e
2cv+lmkb3fYGABxzA+VUEVEsBXprqeJdcBlVdXs3/ZB/jO13ASdSAZrguc2Gknrv
9wg7cPkWIH19m/7DXvQxkX+ROsLSft9AJp3wFzAh4lEGe5zTWaHJS+hfxVrniQEX
M/0qTMRD5PYmsm/8vv6Wx4FlLCx9kWYtygh6Za1h7RDd6iMosDlpV4NASla7fTKV
eOmAvi1uNGxZAoIBAQDyJi3B8wrIu82eZ+LmvVawnIMzK9sk6tJa3vqW3MARO/Lo
Rdh9J4msDGNQRLIgTNW6gFi2dwvcTZJGqpy8oewC83c+STqubMlMxZMkq6PlPtzn
xEdpH8by+IVij/vf4/+vMxPLJgdT+qDjJSnw1eyq++XCJQEf4A4C9MJINoMkxctv
D7DhPjbWlDmrm6i41szYRwEUrF2ayrQtjmwULsVUEsFVqxTK9NJWYPXrVb3EVhNx
X3nKgUh8TYFvesyAl8awm7WIbO4RpZdJ0ftvV0ZihZEVa2GyOfPp8Bx0zZxcuOqE
NrQukl/6ScTJw/MmZ1apMES+AZLGaOgXOl2kShyBAoIBAGiC0xi8rL0JuGXKRbsJ
gqQ2OJYMculq9l7EwPWrXFBkeRUmrXIU3rfeFpHJDWyRIKGHqwpIW38moQDRSVbZ
TB8xBucNye8jzuBplfZkLGA+YLsr8JwOloRJuJZ5IOYp888CKK6g4pxMV8o6tZAb
bkjVxyFimTGiapFMgyLUuy0Y+SatS/ZZP5SNbohLhazI4ulL7CsSPs0LG/xp4axN
+KwvZmkrdH3cqZ12FerUouzPyBnymAsaGKIxDfPl/Phejcxtick8xM9KEw2vr2yQ
uZCXaUfNzhXgC5HJlHFzbZAuq+jBftIiWHRHGkk/8H7YmbJ2i4rv02PyfdVYBd8i
VskCggEANfeuwEYrzuraOl0r60S2XCJURl78s8ug4hkG24/zW+VkjabDw6YY9OEO
INVrlw5dSU7JMz9bqms7fXi7MW+qy5Sfl0OaTGP80I3NsQ8T09OYg0ToKJf/IXHn
GQiGbHRHVTrcjL4blERkzLYlJAr3FuWLIFYusGKFRr/PAeq0GY5yJowaYz3Oqpit
6bf5kM4CK8HQyfMvs0lO3OQ66gRjTq6L/GDHJa+yp6jd7n7p3ocpUBMpTdncCnNb
qYmscnU8UIdrdTGKtySCiBAUup8AzS/Z8D7EnYPg3EiNhG0OJEPSPJPKQqFBnF8w
nL1C44m2dHWfIuDuGDs008g/12A53A==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE0jCCA7qgAwIBAgIBAjANBgkqhkiG9w0BAQsFADCBijELMAkGA1UEBhMCVVMx
EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV
BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G
CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDAeFw0yNDA4MDUyMzEzNTVaFw0z
NDA4MDMyMzEzNTVaMCkxEjAQBgNVBAMMCWxvY2FsaG9zdDETMBEGA1UECgwKYWdn
cmVnYXRlZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMbgGuvCKonn
9KgW58wqgxiykVbbmT82HmhgOBw5CSg7lVnKQlRzfvUtwZX7UhHXYxOCu0no7RqD
Y19QiGOx6SQlaXSMxqPL1PpWMQdX6j/I/H1pHMJ8Ynyd956VlhKUOIQBJMVd46Bg
6IEU4izuJ9uqEuHQrO8lb2eaTXAjsOmtIImxBI7xFsxp42wJyo6ptGetyaNCw7UF
e8xYmBVSLyfiN/drBx/lcwMiDgzfzf8BsavC96Fa/HujXkybAIyVid2DxKODPT9+
7MA1ZY0/Nz3ySKSxIIbfL+1cPihR2GGy1Qwa/GuuJSGAwzy6D/9am5/K3yR927Bk
OgMUCIlpGlSDY1qFd79o/n9BJ9FJmlTMgIZ6RokaXAU2uaiE6v7YPYGkXoIz3ncc
SBprVom2h3RmM3qDbmXio1gGsDL/2SI+Hpnq2zhCzL28K+bZA6ukYpCNyyaYF9O6
X6qi56zGCw/igTJgaYKMsNMSoX+3eHSFxj5JeF4bWcC4qTqmzvfz0HhvX+jjky/V
LHCh9ddH1umT/Ss/IxaNarXxXHoUmN33FgFQ+tML4eTvosG6IxUdJ/34qBT+dWk4
zNIhStTUNiYPbcSfv8qKLHhMAuZOLsTATLsFQt7bzGZwV8HAuCiqzjg9p4R/dRjE
OLhQLZfKkYl99Mgli6v/hooEBSORCulVAgMBAAGjgaIwgZ8wPgYDVR0RBDcwNYIo
djBhbHBoYTEuZXhhbXBsZS5ncmFmYW5hLmFwcC5kZWZhdWx0LnN2Y4IJbG9jYWxo
b3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUFvrZ
B0vSR3B7HcNPCZrZ2H7sFw0wHwYDVR0jBBgwFoAUEGfZJeKIi0lCwlnsxO8ifWzW
3eMwDQYJKoZIhvcNAQELBQADggEBAGYZYrY+eFmgwFp/ohIcWnekdOiCr7qjnmRo
LyNU/EbFz0HUV+yTrtoXLjv8S9D9Yc029Dgj2f31Hp7cOGjUfd0t1DgTBChcFgVr
E5ZbhmKEM0tQiBMI6mHvy6hFT+nc9/yftnndHRUyR4xm6E1dMFqpMyMdYKojRmbn
F6znVTcjBr4OiDnfTUkqYO8kc3I0qvA5ou4jXAJ9mu3UEbEjwc6C2/Mrr48a42Df
rHrwqwnM3DC2+SWVocctk1PRZqMFWypJ9U/HbKPbId79YyHNsI0XtxizV2sX5oXD
1NPPEBeEXZ3Nv/gn4d0h57UoBML9165fRRQSe7CkIl8kZigHGCg=
-----END CERTIFICATE-----

View File

@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIE3jCCAsYCAQAwKTESMBAGA1UEAwwJbG9jYWxob3N0MRMwEQYDVQQKDAphZ2dy
ZWdhdGVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxuAa68Iqief0
qBbnzCqDGLKRVtuZPzYeaGA4HDkJKDuVWcpCVHN+9S3BlftSEddjE4K7SejtGoNj
X1CIY7HpJCVpdIzGo8vU+lYxB1fqP8j8fWkcwnxifJ33npWWEpQ4hAEkxV3joGDo
gRTiLO4n26oS4dCs7yVvZ5pNcCOw6a0gibEEjvEWzGnjbAnKjqm0Z63Jo0LDtQV7
zFiYFVIvJ+I392sHH+VzAyIODN/N/wGxq8L3oVr8e6NeTJsAjJWJ3YPEo4M9P37s
wDVljT83PfJIpLEght8v7Vw+KFHYYbLVDBr8a64lIYDDPLoP/1qbn8rfJH3bsGQ6
AxQIiWkaVINjWoV3v2j+f0En0UmaVMyAhnpGiRpcBTa5qITq/tg9gaRegjPedxxI
GmtWibaHdGYzeoNuZeKjWAawMv/ZIj4emerbOELMvbwr5tkDq6RikI3LJpgX07pf
qqLnrMYLD+KBMmBpgoyw0xKhf7d4dIXGPkl4XhtZwLipOqbO9/PQeG9f6OOTL9Us
cKH110fW6ZP9Kz8jFo1qtfFcehSY3fcWAVD60wvh5O+iwbojFR0n/fioFP51aTjM
0iFK1NQ2Jg9txJ+/yooseEwC5k4uxMBMuwVC3tvMZnBXwcC4KKrOOD2nhH91GMQ4
uFAtl8qRiX30yCWLq/+GigQFI5EK6VUCAwEAAaBwMG4GCSqGSIb3DQEJDjFhMF8w
PgYDVR0RBDcwNYIodjBhbHBoYTEuZXhhbXBsZS5ncmFmYW5hLmFwcC5kZWZhdWx0
LnN2Y4IJbG9jYWxob3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAN
BgkqhkiG9w0BAQsFAAOCAgEAGmr6CnGfcm+DapmhQnQLyq0HyCqKD0PKKamofQLb
YVMiCF/zKDV4+gIs2kMK3uWeZ9r4xDa9nEgBw6U2vi14AI79hmpETCrfvK6hterI
Znb2TQMYnUY3rt26DFNf3/21/jb/1cn/9z55TaHJGqqlJmvB1LfYJoMN8t6A6xg4
J8TYjBuwQQOqFAPRuxmGag44PSC9V5e6gajz56RPyZz2kmdbfPZRNCnqileDWZla
7pilwP8QAhrJCPP25edc/5hP2WNTEH/GTa5FFmkNMKEHn6+dnBuMu5w1SygKUYWz
37qE7jntZC/RGVZ//npwsVyaa+NbgJNjhg/EMj+sWb/Eet2ETq7v9FCM0QG3HNUk
6d6af2YHI3Fo89y5ty1DOydBa5lIxy6gDTwameJYoTem71nPlRtU2b4VFBWZ4xwE
ac7Xmon+Z7tOHVwcCPp1cTwJ2TNwha0JxsW0C1g3QG59ILU5FMPlqOuDJi41uWql
Q56O+a6MnK7GfGxBMMf4FSlbV3xjUGxqyGy5KwIcsy+u/3axCWPUcoz3g0pApbGO
pfsu5Ptr/xMtYjLRXbEcH9Byqx/LrBvD2upwNnlfMtgWIlg/EnZ26Mgardu/NwQq
3fpYv00VWbvBE/B/5p5zmEpA67COFejiwVTsbeN345Ue2mq59DNg6BZmaxKH3Sbq
Kz8=
-----END CERTIFICATE REQUEST-----

View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
C+DUGz2rplZQ/4PtNOcz7reeAFvu6A==
-----END PRIVATE KEY-----

View File

@ -17,6 +17,7 @@ import (
_ "github.com/googleapis/gax-go/v2"
_ "github.com/grafana/dskit/backoff"
_ "github.com/grafana/dskit/flagext"
_ "github.com/grafana/e2e"
_ "github.com/grafana/gofpdf"
_ "github.com/grafana/gomemcache/memcache"
_ "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"

View File

@ -122,22 +122,6 @@ func (dc *databaseCache) Delete(ctx context.Context, key string) error {
})
}
func (dc *databaseCache) Count(ctx context.Context, prefix string) (int64, error) {
res := int64(0)
err := dc.SQLStore.WithDbSession(ctx, func(session *db.Session) error {
sql := "SELECT COUNT(*) FROM cache_data WHERE cache_key LIKE ?"
_, err := session.SQL(sql, prefix+"%").Get(&res)
if err != nil {
return err
}
return nil
})
return res, err
}
// CacheData is the struct representing the table in the database
type CacheData struct {
CacheKey string

View File

@ -6,7 +6,6 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
@ -76,42 +75,3 @@ func TestSecondSet(t *testing.T) {
err = db.Set(context.Background(), "killa-gorilla", obj, 0)
assert.Equal(t, err, nil)
}
func TestDatabaseStorageCount(t *testing.T) {
sqlstore := db.InitTestDB(t)
db := &databaseCache{
SQLStore: sqlstore,
log: log.New("remotecache.database"),
}
obj := []byte("foolbar")
// set time.now to 2 weeks ago
var err error
getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
err = db.Set(context.Background(), "pref-key1", obj, 1000*time.Second)
require.NoError(t, err)
err = db.Set(context.Background(), "pref-key2", obj, 1000*time.Second)
require.NoError(t, err)
err = db.Set(context.Background(), "pref-key3", obj, 1000*time.Second)
require.NoError(t, err)
// insert object that should never expire
err = db.Set(context.Background(), "pref-key4", obj, 0)
require.NoError(t, err)
getTime = time.Now
err = db.Set(context.Background(), "pref-key5", obj, 1000*time.Second)
require.NoError(t, err)
// run GC
db.internalRunGC()
// try to read values
n, errC := db.Count(context.Background(), "pref-")
require.NoError(t, errC)
assert.Equal(t, int64(2), n)
}

View File

@ -57,10 +57,6 @@ func (s *memcachedStorage) Get(ctx context.Context, key string) ([]byte, error)
return memcachedItem.Value, nil
}
func (s *memcachedStorage) Count(ctx context.Context, prefix string) (int64, error) {
return 0, ErrNotImplemented
}
// Delete delete a key from the cache
func (s *memcachedStorage) Delete(ctx context.Context, key string) error {
return s.c.Delete(key)

View File

@ -20,5 +20,4 @@ func TestIntegrationMemcachedCacheStorage(t *testing.T) {
opts := &setting.RemoteCacheOptions{Name: memcachedCacheType, ConnStr: u}
client := createTestClient(t, opts, nil)
runTestsForClient(t, client)
runCountTestsForClient(t, opts, nil)
}

View File

@ -110,12 +110,3 @@ func (s *redisStorage) Delete(ctx context.Context, key string) error {
cmd := s.c.Del(ctx, key)
return cmd.Err()
}
func (s *redisStorage) Count(ctx context.Context, prefix string) (int64, error) {
cmd := s.c.Keys(ctx, prefix+"*")
if cmd.Err() != nil {
return 0, cmd.Err()
}
return int64(len(cmd.Val())), nil
}

View File

@ -37,5 +37,4 @@ func TestIntegrationRedisCacheStorage(t *testing.T) {
opts := &setting.RemoteCacheOptions{Name: redisCacheType, ConnStr: b.String()}
client := createTestClient(t, opts, nil)
runTestsForClient(t, client)
runCountTestsForClient(t, opts, nil)
}

View File

@ -69,11 +69,6 @@ type CacheStorage interface {
// Delete object from cache
Delete(ctx context.Context, key string) error
// Count returns the number of items in the cache.
// Optionaly a prefix can be provided to only count items with that prefix
// DO NOT USE. Not available for memcached.
Count(ctx context.Context, prefix string) (int64, error)
}
// RemoteCache allows Grafana to cache data outside its own process
@ -102,11 +97,6 @@ func (ds *RemoteCache) Delete(ctx context.Context, key string) error {
return ds.client.Delete(ctx, key)
}
// Count returns the number of items in the cache.
func (ds *RemoteCache) Count(ctx context.Context, prefix string) (int64, error) {
return ds.client.Count(ctx, prefix)
}
// Run starts the backend processes for cache clients.
func (ds *RemoteCache) Run(ctx context.Context) error {
// create new interface if more clients need GC jobs
@ -173,10 +163,6 @@ func (pcs *encryptedCacheStorage) Delete(ctx context.Context, key string) error
return pcs.cache.Delete(ctx, key)
}
func (pcs *encryptedCacheStorage) Count(ctx context.Context, prefix string) (int64, error) {
return pcs.cache.Count(ctx, prefix)
}
type prefixCacheStorage struct {
cache CacheStorage
prefix string
@ -191,7 +177,3 @@ func (pcs *prefixCacheStorage) Set(ctx context.Context, key string, value []byte
func (pcs *prefixCacheStorage) Delete(ctx context.Context, key string) error {
return pcs.cache.Delete(ctx, pcs.prefix+key)
}
func (pcs *prefixCacheStorage) Count(ctx context.Context, prefix string) (int64, error) {
return pcs.cache.Count(ctx, pcs.prefix+prefix)
}

View File

@ -42,7 +42,6 @@ func TestCachedBasedOnConfig(t *testing.T) {
client := createTestClient(t, cfg.RemoteCacheOptions, db)
runTestsForClient(t, client)
runCountTestsForClient(t, cfg.RemoteCacheOptions, db)
}
func TestInvalidCacheTypeReturnsError(t *testing.T) {
@ -55,37 +54,6 @@ func runTestsForClient(t *testing.T, client CacheStorage) {
canNotFetchExpiredItems(t, client)
}
func runCountTestsForClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore db.DB) {
client := createTestClient(t, opts, sqlstore)
expectError := false
if opts.Name == memcachedCacheType {
expectError = true
}
t.Run("can count items", func(t *testing.T) {
cacheableValue := []byte("hej hej")
err := client.Set(context.Background(), "pref-key1", cacheableValue, 0)
require.NoError(t, err)
err = client.Set(context.Background(), "pref-key2", cacheableValue, 0)
require.NoError(t, err)
err = client.Set(context.Background(), "key3-not-pref", cacheableValue, 0)
require.NoError(t, err)
n, errC := client.Count(context.Background(), "pref-")
if expectError {
require.ErrorIs(t, ErrNotImplemented, errC)
assert.Equal(t, int64(0), n)
return
}
require.NoError(t, errC)
assert.Equal(t, int64(2), n)
})
}
func canPutGetAndDeleteCachedObjects(t *testing.T, client CacheStorage) {
dataToCache := []byte("some bytes")

View File

@ -28,10 +28,6 @@ func (fcs FakeCacheStorage) Delete(_ context.Context, key string) error {
return nil
}
func (fcs FakeCacheStorage) Count(_ context.Context, prefix string) (int64, error) {
return int64(len(fcs.Storage)), nil
}
func NewFakeCacheStorage() FakeCacheStorage {
return FakeCacheStorage{
Storage: map[string][]byte{},

View File

@ -237,7 +237,7 @@ func readPluginJSON(pluginDir string) (plugins.JSONData, error) {
// nolint:gosec
data, err = os.ReadFile(pluginPath)
if err != nil {
return plugins.JSONData{}, fmt.Errorf("could not find plugin.json or dist/plugin.json for in %s", pluginDir)
return plugins.JSONData{}, fmt.Errorf("could not find plugin.json or dist/plugin.json in %s", pluginDir)
}
}

View File

@ -8,6 +8,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@ -43,11 +44,16 @@ func convertToK8sResource(orgID int64, receiver definitions.GettableApiReceiver,
return nil, fmt.Errorf("all integrations must have the same provenance")
}
provenance = integration.Provenance
unstruct := common.Unstructured{}
err := json.Unmarshal(integration.Settings, &unstruct)
if err != nil {
return nil, fmt.Errorf("integration '%s' of receiver '%s' has settings that cannot be parsed as JSON: %w", integration.Type, receiver.Name, err)
}
spec.Integrations = append(spec.Integrations, model.Integration{
Uid: &integration.UID,
Type: integration.Type,
DisableResolveMessage: &integration.DisableResolveMessage,
Settings: json.RawMessage(integration.Settings),
Settings: unstruct,
SecureFields: integration.SecureFields,
})
}
@ -79,12 +85,16 @@ func convertToDomainModel(receiver *model.Receiver) (definitions.GettableApiRece
}
for _, integration := range receiver.Spec.Integrations {
data, err := integration.Settings.MarshalJSON()
if err != nil {
return definitions.GettableApiReceiver{}, fmt.Errorf("integration '%s' of receiver '%s' is invalid: failed to convert unstructured data to bytes: %w", integration.Type, receiver.Name, err)
}
grafanaIntegration := definitions.GettableGrafanaReceiver{
Name: receiver.Spec.Title,
Type: integration.Type,
Settings: definitions.RawMessage(integration.Settings),
Settings: definitions.RawMessage(data),
SecureFields: integration.SecureFields,
//Provenance: "", //TODO: Convert provenance?
Provenance: definitions.Provenance(models.ProvenanceNone),
}
if integration.Uid != nil {
grafanaIntegration.UID = *integration.Uid

View File

@ -1,19 +1,14 @@
package receiver
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
)
var _ grafanarest.Storage = (*storage)(nil)
@ -35,25 +30,9 @@ func NewStorage(
dualWriteBuilder grafanarest.DualWriteBuilder,
) (rest.Storage, error) {
legacyStore := &legacyStorage{
service: legacySvc,
namespacer: namespacer,
tableConverter: utils.NewTableConverter(
resourceInfo.GroupResource(),
[]metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Title", Type: "string", Format: "string", Description: "The receiver name"}, // TODO: Add integration types.
},
func(obj any) ([]interface{}, error) {
r, ok := obj.(*model.Receiver)
if ok {
return []interface{}{
r.Name,
r.Spec.Title,
// r.Spec, //TODO implement formatting for Spec, same as UI?
}, nil
}
return nil, fmt.Errorf("expected resource or info")
}),
service: legacySvc,
namespacer: namespacer,
tableConverter: resourceInfo.TableConverter(),
}
if optsGetter != nil && dualWriteBuilder != nil {
strategy := grafanaregistry.NewStrategy(scheme)

View File

@ -13,6 +13,7 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
notificationsModels "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
@ -55,11 +56,11 @@ func RegisterAPIService(
return builder
}
func (t NotificationsAPIBuilder) GetGroupVersion() schema.GroupVersion {
func (t *NotificationsAPIBuilder) GetGroupVersion() schema.GroupVersion {
return t.gv
}
func (t NotificationsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
func (t *NotificationsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
err := notificationsModels.AddToScheme(scheme)
if err != nil {
return err
@ -67,7 +68,7 @@ func (t NotificationsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
return scheme.SetVersionPriority(notificationsModels.SchemeGroupVersion)
}
func (t NotificationsAPIBuilder) GetAPIGroupInfo(
func (t *NotificationsAPIBuilder) GetAPIGroupInfo(
scheme *runtime.Scheme,
codecs serializer.CodecFactory,
optsGetter generic.RESTOptionsGetter,
@ -92,15 +93,35 @@ func (t NotificationsAPIBuilder) GetAPIGroupInfo(
return &apiGroupInfo, nil
}
func (t NotificationsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
func (t *NotificationsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
return notificationsModels.GetOpenAPIDefinitions
}
func (t NotificationsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
func (t *NotificationsAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
return nil
}
func (t NotificationsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
// PostProcessOpenAPI is a hook to alter OpenAPI3 specification of the API server.
func (t *NotificationsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
// The plugin description
oas.Info.Description = "Grafana Alerting Notification resources"
// The root api URL
root := "/apis/" + t.GetGroupVersion().String() + "/"
// Hide the ability to list or watch across all tenants
delete(oas.Paths.Paths, root+notificationsModels.ReceiverResourceInfo.GroupResource().Resource)
delete(oas.Paths.Paths, root+notificationsModels.TimeIntervalResourceInfo.GroupResource().Resource)
// The root API discovery list
sub := oas.Paths.Paths[root]
if sub != nil && sub.Get != nil {
sub.Get.Tags = []string{"API Discovery"} // sorts first in the list
}
return oas, nil
}
func (t *NotificationsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
return authorizer.AuthorizerFunc(
func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
switch a.GetResource() {

View File

@ -10,6 +10,7 @@ import (
model "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
func convertToK8sResources(orgID int64, intervals []definitions.MuteTimeInterval, namespacer request.NamespaceMapper, selector fields.Selector) (*model.TimeIntervalList, error) {
@ -77,6 +78,7 @@ func convertToDomainModel(interval *model.TimeInterval) (definitions.MuteTimeInt
}
result.Version = interval.ResourceVersion
result.UID = interval.ObjectMeta.Name
result.Provenance = definitions.Provenance(models.ProvenanceNone)
err = result.Validate()
if err != nil {
return definitions.MuteTimeInterval{}, err

View File

@ -3,7 +3,6 @@ package timeinterval
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
@ -16,7 +15,6 @@ import (
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
)
var _ grafanarest.Storage = (*storage)(nil)
@ -38,24 +36,9 @@ func NewStorage(
dualWriteBuilder grafanarest.DualWriteBuilder,
) (rest.Storage, error) {
legacyStore := &legacyStorage{
service: legacySvc,
namespacer: namespacer,
tableConverter: utils.NewTableConverter(
resourceInfo.GroupResource(),
[]metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
// {Name: "Intervals", Type: "string", Format: "string", Description: "The display name"},
},
func(obj any) ([]interface{}, error) {
r, ok := obj.(*model.TimeInterval)
if ok {
return []interface{}{
r.Name,
// r.Spec, //TODO implement formatting for Spec, same as UI?
}, nil
}
return nil, fmt.Errorf("expected resource or info")
}),
service: legacySvc,
namespacer: namespacer,
tableConverter: resourceInfo.TableConverter(),
}
if optsGetter != nil && dualWriteBuilder != nil {
strategy := grafanaregistry.NewStrategy(scheme)

View File

@ -0,0 +1,38 @@
package legacy
import (
"embed"
"fmt"
"text/template"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
// Templates setup.
var (
//go:embed *.sql
sqlTemplatesFS embed.FS
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `*.sql`))
)
func mustTemplate(filename string) *template.Template {
if t := sqlTemplates.Lookup(filename); t != nil {
return t
}
panic(fmt.Sprintf("template file not found: %s", filename))
}
// Templates.
var (
sqlQueryDashboards = mustTemplate("query_dashboards.sql")
)
type sqlQuery struct {
*sqltemplate.SQLTemplate
Query *DashboardQuery
}
func (r sqlQuery) Validate() error {
return nil // TODO
}

View File

@ -0,0 +1,167 @@
package legacy
import (
"embed"
"os"
"path/filepath"
"testing"
"text/template"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
//go:embed testdata/*
var testdataFS embed.FS
func testdata(t *testing.T, filename string) []byte {
t.Helper()
b, err := testdataFS.ReadFile(`testdata/` + filename)
if err != nil {
writeTestData(filename, "<empty>")
assert.Fail(t, "missing test file")
}
return b
}
func writeTestData(filename, value string) {
_ = os.WriteFile(filepath.Join("testdata", filename), []byte(value), 0777)
}
func TestQueries(t *testing.T) {
t.Parallel()
// Check each dialect
dialects := []sqltemplate.Dialect{
sqltemplate.MySQL,
sqltemplate.SQLite,
sqltemplate.PostgreSQL,
}
// Each template has one or more test cases, each identified with a
// descriptive name (e.g. "happy path", "error twiddling the frobb"). Each
// of them will test that for the same input data they must produce a result
// that will depend on the Dialect. Expected queries should be defined in
// separate files in the testdata directory. This improves the testing
// experience by separating test data from test code, since mixing both
// tends to make it more difficult to reason about what is being done,
// especially as we want testing code to scale and make it easy to add
// tests.
type (
testCase = struct {
Name string
// Data should be the struct passed to the template.
Data sqltemplate.SQLTemplateIface
}
)
// Define tests cases. Most templates are trivial and testing that they
// generate correct code for a single Dialect is fine, since the one thing
// that always changes is how SQL placeholder arguments are passed (most
// Dialects use `?` while PostgreSQL uses `$1`, `$2`, etc.), and that is
// something that should be tested in the Dialect implementation instead of
// here. We will ask to have at least one test per SQL template, and we will
// lean to test MySQL. Templates containing branching (conditionals, loops,
// etc.) should be exercised at least once in each of their branches.
//
// NOTE: in the Data field, make sure to have pointers populated to simulate
// data is set as it would be in a real request. The data being correctly
// populated in each case should be tested in integration tests, where the
// data will actually flow to and from a real database. In this tests we
// only care about producing the correct SQL.
testCases := map[*template.Template][]*testCase{
sqlQueryDashboards: {
{
Name: "history_uid",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
UID: "UUU",
},
},
},
{
Name: "history_uid_at_version",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
UID: "UUU",
Version: 3,
},
},
},
{
Name: "history_uid_second_page",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
UID: "UUU",
LastID: 7,
},
},
},
{
Name: "dashboard",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
},
},
},
{
Name: "dashboard_next_page",
Data: &sqlQuery{
SQLTemplate: new(sqltemplate.SQLTemplate),
Query: &DashboardQuery{
OrgID: 2,
LastID: 22,
},
},
},
},
}
// Execute test cases
for tmpl, tcs := range testCases {
t.Run(tmpl.Name(), func(t *testing.T) {
t.Parallel()
for _, tc := range tcs {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
for _, dialect := range dialects {
filename := dialect.DialectName() + "__" + tc.Name + ".sql"
t.Run(filename, func(t *testing.T) {
// not parallel because we're sharing tc.Data, not
// worth it deep cloning
expectedQuery := string(testdata(t, filename))
//expectedQuery := sqltemplate.FormatSQL(rawQuery)
tc.Data.SetDialect(dialect)
err := tc.Data.Validate()
require.NoError(t, err)
got, err := sqltemplate.Execute(tmpl, tc.Data)
require.NoError(t, err)
got = sqltemplate.RemoveEmptyLines(got)
if diff := cmp.Diff(expectedQuery, got); diff != "" {
writeTestData(filename, got)
t.Errorf("%s: %s", tc.Name, diff)
}
})
}
})
}
})
}
}

View File

@ -0,0 +1,45 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
{{ if .Query.UseHistoryTable }}
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
{{ else }}
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
{{ end }}
FROM dashboard
{{ if .Query.UseHistoryTable }}
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
{{ end }}
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN {{ .Ident "user" }} AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN {{ .Ident "user" }} AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = {{ .Arg .Query.OrgID }}
{{ if .Query.UseHistoryTable }}
{{ if .Query.Version }}
AND dashboard_version.version = {{ .Arg .Query.Version }}
{{ else if .Query.LastID }}
AND dashboard_version.version < {{ .Arg .Query.LastID }}
{{ end }}
ORDER BY dashboard_version.version DESC
{{ else }}
{{ if .Query.UID }}
AND dashboard.uid = {{ .Arg .Query.UID }}
{{ else if .Query.LastID }}
AND dashboard.id > {{ .Arg .Query.LastID }}
{{ end }}
{{ if .Query.GetTrash }}
AND dashboard.deleted IS NOT NULL
{{ else if .Query.LastID }}
AND dashboard.deleted IS NULL
{{ end }}
ORDER BY dashboard.id DESC
{{ end }}

View File

@ -9,9 +9,6 @@ import (
"sync"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
@ -21,8 +18,12 @@ import (
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
)
var (
@ -46,10 +47,12 @@ type dashboardRow struct {
type dashboardSqlAccess struct {
sql db.DB
dialect sqltemplate.Dialect
sess *session.SessionDB
namespacer request.NamespaceMapper
dashStore dashboards.Store
provisioning provisioning.ProvisioningService
currentRV func(ctx context.Context) (int64, error)
// Typically one... the server wrapper
subscribers []chan *resource.WrittenEvent
@ -61,131 +64,82 @@ func NewDashboardAccess(sql db.DB,
dashStore dashboards.Store,
provisioning provisioning.ProvisioningService,
) DashboardAccess {
dialect := sqltemplate.DialectForDriver(string(sql.GetDBType()))
if dialect == nil {
// panic?
// fmt.Errorf("no dialect for driver %q", driverName)
fmt.Printf("ERROR: NO DIALECT")
}
sess := sql.GetSqlxSession()
currentRV := func(ctx context.Context) (int64, error) {
t := time.Now()
max := ""
err := sess.Get(ctx, &max, "SELECT MAX(updated) FROM dashboard")
if err == nil && max != "" {
t, _ = time.Parse(time.DateTime, max) // ignore null errors
}
return t.UnixMilli(), nil
}
if sql.GetDBType() == migrator.Postgres {
currentRV = func(ctx context.Context) (int64, error) {
max := time.Now()
_ = sess.Get(ctx, &max, "SELECT MAX(updated) FROM dashboard")
return max.UnixMilli(), nil
}
} else if sql.GetDBType() == migrator.MySQL {
currentRV = func(ctx context.Context) (int64, error) {
max := time.Now().UnixMilli()
_ = sess.Get(ctx, &max, "SELECT UNIX_TIMESTAMP(MAX(updated)) FROM dashboard;")
return max, nil
}
}
return &dashboardSqlAccess{
sql: sql,
sess: sql.GetSqlxSession(),
sess: sess,
dialect: dialect,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
currentRV: currentRV,
}
}
func (a *dashboardSqlAccess) currentRV(ctx context.Context) (int64, error) {
t := time.Now()
max := ""
err := a.sess.Get(ctx, &max, "SELECT MAX(updated) FROM dashboard")
if err == nil && max != "" {
t, err = time.Parse(time.DateTime, max)
}
return t.UnixMilli(), err
}
const selector = `SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.created,CreatedUSER.uid as created_by,
dashboard.updated,UpdatedUSER.uid as updated_by,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.version, '', dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN user AS CreatedUSER ON dashboard.created_by = CreatedUSER.id
LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.updated_by = UpdatedUSER.id
WHERE dashboard.is_folder = false`
const history = `SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.created,CreatedUSER.uid as created_by,
dashboard_version.created,UpdatedUSER.uid as updated_by,
NULL, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN user AS CreatedUSER ON dashboard.created_by = CreatedUSER.id
LEFT OUTER JOIN user AS UpdatedUSER ON dashboard_version.created_by = UpdatedUSER.id
WHERE dashboard.is_folder = false`
func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery) (*rowsWrapper, error) {
if len(query.Labels) > 0 {
return nil, fmt.Errorf("labels not yet supported")
// if query.Requirements.Folder != nil {
// args = append(args, *query.Requirements.Folder)
// sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=$%d", sqlcmd, len(args))
// sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=?$%d", sqlcmd, len(args))
// }
}
var sqlcmd string
args := []any{query.OrgID}
if query.GetHistory || query.Version > 0 {
if query.GetTrash {
return nil, fmt.Errorf("trash not included in history table")
}
sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d\n ", history, len(args))
if query.UID == "" {
return nil, fmt.Errorf("history query must have a UID")
}
args = append(args, query.UID)
sqlcmd = fmt.Sprintf("%s AND dashboard.uid=$%d", sqlcmd, len(args))
if query.Version > 0 {
args = append(args, query.Version)
sqlcmd = fmt.Sprintf("%s AND dashboard_version.version=$%d", sqlcmd, len(args))
} else if query.LastID > 0 {
args = append(args, query.LastID)
sqlcmd = fmt.Sprintf("%s AND dashboard_version.version<$%d", sqlcmd, len(args))
}
sqlcmd = fmt.Sprintf("%s\n ORDER BY dashboard_version.version desc", sqlcmd)
} else {
sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d\n ", selector, len(args))
if query.UID != "" {
args = append(args, query.UID)
sqlcmd = fmt.Sprintf("%s AND dashboard.uid=$%d", sqlcmd, len(args))
} else if query.LastID > 0 {
args = append(args, query.LastID)
sqlcmd = fmt.Sprintf("%s AND dashboard.id>$%d", sqlcmd, len(args))
}
if query.GetTrash {
sqlcmd = sqlcmd + " AND dashboard.deleted IS NOT NULL"
} else {
sqlcmd = sqlcmd + " AND dashboard.deleted IS NULL"
}
sqlcmd = fmt.Sprintf("%s\n ORDER BY dashboard.id asc", sqlcmd)
req := sqlQuery{
SQLTemplate: sqltemplate.New(a.dialect),
Query: query,
}
// fmt.Printf("%s // %v\n", sqlcmd, args)
rows, err := a.doQuery(ctx, sqlcmd, args...)
tmpl := sqlQueryDashboards
if query.UseHistoryTable() && query.GetTrash {
return nil, fmt.Errorf("trash not included in history table")
}
rawQuery, err := sqltemplate.Execute(tmpl, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", tmpl.Name(), err)
}
q := rawQuery
// q = sqltemplate.RemoveEmptyLines(rawQuery)
// fmt.Printf(">>%s [%+v]", q, req.GetArgs())
rows, err := a.sess.Query(ctx, q, req.GetArgs()...)
if err != nil {
if rows != nil {
_ = rows.Close()
}
rows = nil
}
return rows, err
}
func (a *dashboardSqlAccess) doQuery(ctx context.Context, query string, args ...any) (*rowsWrapper, error) {
_, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
rows, err := a.sess.Query(ctx, query, args...)
return &rowsWrapper{
rows: rows,
a: a,
@ -211,6 +165,9 @@ type rowsWrapper struct {
}
func (r *rowsWrapper) Close() error {
if r.rows == nil {
return nil
}
return r.rows.Close()
}
@ -291,10 +248,12 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
var folder_uid sql.NullString
var updated time.Time
var updatedBy sql.NullString
var updatedByID sql.NullInt64
var deleted sql.NullTime
var created time.Time
var createdBy sql.NullString
var createdByID sql.NullInt64
var message sql.NullString
var plugin_id string
@ -306,10 +265,10 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
var version int64
err := rows.Scan(&orgId, &dashboard_id, &dash.Name, &folder_uid,
&created, &createdBy,
&updated, &updatedBy,
&deleted, &plugin_id,
&origin_name, &origin_path, &origin_hash, &origin_ts,
&created, &createdBy, &createdByID,
&updated, &updatedBy, &updatedByID,
&version, &message, &data,
)
@ -325,8 +284,8 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
return nil, err
}
meta.SetUpdatedTimestamp(&updated)
meta.SetCreatedBy(getUserID(createdBy))
meta.SetUpdatedBy(getUserID(updatedBy))
meta.SetCreatedBy(getUserID(createdBy, createdByID))
meta.SetUpdatedBy(getUserID(updatedBy, updatedByID))
if deleted.Valid {
meta.SetDeletionTimestamp(ptr.To(metav1.NewTime(deleted.Time)))
@ -377,11 +336,14 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
return row, err
}
func getUserID(v sql.NullString) string {
if v.String == "" {
func getUserID(v sql.NullString, id sql.NullInt64) string {
if v.Valid && v.String != "" {
return identity.NewTypedIDString(identity.TypeUser, v.String).String()
}
if id.Valid && id.Int64 == -1 {
return identity.NewTypedIDString(identity.TypeProvisioning, "").String()
}
return identity.NewTypedIDString(identity.TypeUser, v.String).String()
return ""
}
// DeleteDashboard implements DashboardAccess.

View File

@ -147,13 +147,13 @@ func (a *dashboardSqlAccess) ReadResource(ctx context.Context, req *resource.Rea
rsp.Error = &resource.ErrorResult{
Code: http.StatusNotFound,
}
} else {
rsp.Value, err = json.Marshal(dash)
if err != nil {
rsp.Error = resource.AsErrorResult(err)
}
}
rsp.ResourceVersion = rv
rsp.Value, err = json.Marshal(dash)
if err != nil {
rsp.Error = resource.AsErrorResult(err)
}
return rsp
}
@ -177,11 +177,10 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resource.Lis
}
query := &DashboardQuery{
OrgID: info.OrgID,
Limit: int(req.Limit),
MaxBytes: 2 * 1024 * 1024, // 2MB,
LastID: token.id,
Labels: req.Options.Labels,
OrgID: info.OrgID,
Limit: int(req.Limit),
LastID: token.id,
Labels: req.Options.Labels,
}
listRV, err := a.currentRV(ctx)
@ -194,7 +193,7 @@ func (a *dashboardSqlAccess) ListIterator(ctx context.Context, req *resource.Lis
_ = rows.Close()
}()
}
if err != nil {
if err == nil {
err = cb(rows)
}
return listRV, err
@ -253,13 +252,15 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
if token.orgId > 0 && token.orgId != info.OrgID {
return nil, fmt.Errorf("token and orgID mismatch")
}
limit := int(req.Limit)
if limit < 1 {
limit = 15
}
query := &DashboardQuery{
OrgID: info.OrgID,
Limit: int(req.Limit),
MaxBytes: 2 * 1024 * 1024, // 2MB,
LastID: token.id,
UID: req.Key.Name,
OrgID: info.OrgID,
Limit: limit + 1,
LastID: token.id,
UID: req.Key.Name,
}
if req.ShowDeleted {
query.GetTrash = true
@ -273,7 +274,6 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
}
defer func() { _ = rows.Close() }()
totalSize := 0
list := &resource.HistoryResponse{}
for rows.Next() {
if rows.err != nil || rows.row == nil {
@ -291,8 +291,7 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
return list, err
}
totalSize += len(rows.Value())
if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= query.Limit) {
if len(list.Items) >= limit {
// if query.Requirements.Folder != nil {
// row.token.folder = *query.Requirements.Folder
// }

View File

@ -0,0 +1,18 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.id > ?
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,19 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.uid = ?
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM dashboard
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard_version.version = ?
ORDER BY dashboard_version.version DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN `user` AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `user` AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = ?
AND dashboard.uid = ?
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,18 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
AND dashboard.id > $2
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,19 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
AND dashboard.uid = $2
ORDER BY dashboard.id DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard_version.created, updated_user.uid as updated_by,updated_user.id as created_by_id,
dashboard_version.version, dashboard_version.message, dashboard_version.data
FROM dashboard
LEFT OUTER JOIN dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
AND dashboard_version.version = $2
ORDER BY dashboard_version.version DESC

View File

@ -0,0 +1,20 @@
SELECT
dashboard.org_id, dashboard.id,
dashboard.uid, dashboard.folder_uid,
dashboard.deleted, plugin_id,
dashboard_provisioning.name as origin_name,
dashboard_provisioning.external_id as origin_path,
dashboard_provisioning.check_sum as origin_key,
dashboard_provisioning.updated as origin_ts,
dashboard.created, created_user.uid as created_by, dashboard.created_by as created_by_id,
dashboard.updated, updated_user.uid as updated_by, dashboard.updated_by as updated_by_id,
dashboard.version, '' as message, dashboard.data
FROM dashboard
LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
LEFT OUTER JOIN "user" AS created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "user" AS updated_user ON dashboard.updated_by = updated_user.id
WHERE dashboard.is_folder = false
AND dashboard.org_id = $1
AND dashboard.uid = $2
AND dashboard.deleted IS NULL
ORDER BY dashboard.id DESC

Some files were not shown because too many files have changed in this diff Show More