mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'main' into drclau/unistor/replace-authenticators-3
This commit is contained in:
commit
0fdd2ff802
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@ -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
|
||||
|
2
.github/workflows/go_lint.yml
vendored
2
.github/workflows/go_lint.yml
vendored
@ -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
|
||||
|
894
.yarn/releases/yarn-4.3.1.cjs
vendored
894
.yarn/releases/yarn-4.3.1.cjs
vendored
File diff suppressed because one or more lines are too long
925
.yarn/releases/yarn-4.4.0.cjs
vendored
Executable file
925
.yarn/releases/yarn-4.4.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
@ -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:
|
||||
|
@ -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 \
|
||||
|
2
Makefile
2
Makefile
@ -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)
|
||||
|
@ -65,6 +65,7 @@ enable_gzip = false
|
||||
# https certs & key file
|
||||
cert_file =
|
||||
cert_key =
|
||||
cert_pass =
|
||||
|
||||
# Certificates file watch interval
|
||||
certs_watch_interval =
|
||||
|
@ -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 =
|
||||
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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 }}
|
||||
|
@ -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" >}}
|
||||
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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 |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
@ -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
|
||||
---
|
||||
|
||||
|
@ -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 |
|
||||
|
@ -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 |
|
||||
|
@ -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 you’re 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
7
go.mod
@ -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
18
go.sum
@ -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=
|
||||
|
1
go.work
1
go.work
@ -7,6 +7,7 @@ use (
|
||||
./pkg/build
|
||||
./pkg/build/wire
|
||||
./pkg/promlib
|
||||
./pkg/semconv
|
||||
./pkg/storage/unified/resource
|
||||
./pkg/util/xorm
|
||||
)
|
||||
|
@ -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=
|
||||
|
@ -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/*.*
|
||||
|
15
package.json
15
package.json
@ -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
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -20,5 +20,5 @@
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.91.0"
|
||||
},
|
||||
"packageManager": "yarn@4.3.1"
|
||||
"packageManager": "yarn@4.4.0"
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -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,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
@ -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 } },
|
||||
|
@ -12,7 +12,7 @@ export function getPageStyles(theme: GrafanaTheme2) {
|
||||
? {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100vh',
|
||||
height: '100vh',
|
||||
}
|
||||
: {
|
||||
display: 'flex',
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
@ -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.
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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"},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
6
pkg/cmd/grafana/apiserver/testdata/certificates/README.md
vendored
Normal file
6
pkg/cmd/grafana/apiserver/testdata/certificates/README.md
vendored
Normal 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.
|
24
pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt
vendored
Executable file
24
pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt
vendored
Executable 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-----
|
28
pkg/cmd/grafana/apiserver/testdata/certificates/ca.key
vendored
Executable file
28
pkg/cmd/grafana/apiserver/testdata/certificates/ca.key
vendored
Executable 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-----
|
27
pkg/cmd/grafana/apiserver/testdata/certificates/client.crt
vendored
Executable file
27
pkg/cmd/grafana/apiserver/testdata/certificates/client.crt
vendored
Executable 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-----
|
27
pkg/cmd/grafana/apiserver/testdata/certificates/client.csr
vendored
Executable file
27
pkg/cmd/grafana/apiserver/testdata/certificates/client.csr
vendored
Executable 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-----
|
52
pkg/cmd/grafana/apiserver/testdata/certificates/client.key
vendored
Executable file
52
pkg/cmd/grafana/apiserver/testdata/certificates/client.key
vendored
Executable file
@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
nL1C44m2dHWfIuDuGDs008g/12A53A==
|
||||
-----END PRIVATE KEY-----
|
28
pkg/cmd/grafana/apiserver/testdata/certificates/server.crt
vendored
Executable file
28
pkg/cmd/grafana/apiserver/testdata/certificates/server.crt
vendored
Executable 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-----
|
29
pkg/cmd/grafana/apiserver/testdata/certificates/server.csr
vendored
Executable file
29
pkg/cmd/grafana/apiserver/testdata/certificates/server.csr
vendored
Executable 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-----
|
52
pkg/cmd/grafana/apiserver/testdata/certificates/server.key
vendored
Executable file
52
pkg/cmd/grafana/apiserver/testdata/certificates/server.key
vendored
Executable file
@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
C+DUGz2rplZQ/4PtNOcz7reeAFvu6A==
|
||||
-----END PRIVATE KEY-----
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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{},
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
38
pkg/registry/apis/dashboard/legacy/queries.go
Normal file
38
pkg/registry/apis/dashboard/legacy/queries.go
Normal 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
|
||||
}
|
167
pkg/registry/apis/dashboard/legacy/queries_test.go
Normal file
167
pkg/registry/apis/dashboard/legacy/queries_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
45
pkg/registry/apis/dashboard/legacy/query_dashboards.sql
Normal file
45
pkg/registry/apis/dashboard/legacy/query_dashboards.sql
Normal 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 }}
|
@ -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.
|
||||
|
@ -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
|
||||
// }
|
||||
|
18
pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard.sql
vendored
Executable file
18
pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard.sql
vendored
Executable 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
|
20
pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard_next_page.sql
vendored
Executable file
20
pkg/registry/apis/dashboard/legacy/testdata/mysql__dashboard_next_page.sql
vendored
Executable 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
|
19
pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid.sql
vendored
Executable file
19
pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid.sql
vendored
Executable 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
|
20
pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_at_version.sql
vendored
Executable file
20
pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_at_version.sql
vendored
Executable 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
|
20
pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_second_page.sql
vendored
Executable file
20
pkg/registry/apis/dashboard/legacy/testdata/mysql__history_uid_second_page.sql
vendored
Executable 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
|
18
pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard.sql
vendored
Executable file
18
pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard.sql
vendored
Executable 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
|
20
pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard_next_page.sql
vendored
Executable file
20
pkg/registry/apis/dashboard/legacy/testdata/postgres__dashboard_next_page.sql
vendored
Executable 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
|
19
pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid.sql
vendored
Executable file
19
pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid.sql
vendored
Executable 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
|
20
pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_at_version.sql
vendored
Executable file
20
pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_at_version.sql
vendored
Executable 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
|
20
pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_second_page.sql
vendored
Executable file
20
pkg/registry/apis/dashboard/legacy/testdata/postgres__history_uid_second_page.sql
vendored
Executable 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
Loading…
Reference in New Issue
Block a user