Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
This commit is contained in:
commit
6c83756539
121 files changed
+2511
-1329
No files matched your search
@@ -705,7 +705,6 @@ embed.go @grafana/grafana-as-code
|
||||
/.github/pr-checks.json @tolzhabayev
|
||||
/.github/pr-commands.json @tolzhabayev
|
||||
/.github/renovate.json5 @grafana/frontend-ops
|
||||
/.github/teams.yml @armandgrillet
|
||||
/.github/workflows/alerting-swagger-gen.yml @grafana/alerting-backend
|
||||
/.github/workflows/auto-milestone.yml @grafana/grafana-developer-enablement-squad
|
||||
/.github/workflows/backport.yml @grafana/grafana-developer-enablement-squad
|
||||
@@ -722,7 +721,6 @@ embed.go @grafana/grafana-as-code
|
||||
/.github/workflows/deploy-pr-preview.yml @grafana/docs-tooling
|
||||
/.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina
|
||||
/.github/workflows/github-release.yml @grafana/grafana-developer-enablement-squad
|
||||
/.github/workflows/issue-labeled.yml @armandgrillet
|
||||
/.github/workflows/issue-opened.yml @grafana/grafana-community-support
|
||||
/.github/workflows/metrics-collector.yml @torkelo
|
||||
/.github/workflows/milestone.yml @tolzhabayev
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
# The first keys are labels used on issues. All fields are optional.
|
||||
# Example
|
||||
test:
|
||||
# channel-label is used to send a message to a specific channel
|
||||
# when the label "test" is added to an issue.
|
||||
channel-label: CXXXXXXXXXX
|
||||
|
||||
# Alerting team
|
||||
area/alerting:
|
||||
channel-label: C028MCV4R7C
|
||||
exclude-github-team: alerting-squad
|
||||
|
||||
# DataViz squad
|
||||
area/dataviz:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/legend:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/barchart:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/bargauge:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/candlestick:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/canvas:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/common:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/gauge:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/geomap:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/graph:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/heatmap:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/histogram:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/piechart:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/stat:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/state-timeline:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/status-history:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/timeseries:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/trend:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/panel/xychart:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/tooltip:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/transformations:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/value-mapping:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
|
||||
area/units:
|
||||
channel-label: C04J73AAQ87 # grafana-dataviz
|
||||
exclude-github-team: dataviz-squad
|
||||
@@ -1,99 +0,0 @@
|
||||
name: Notify Slack channel based on new issue label
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: "ubuntu-latest"
|
||||
outputs:
|
||||
has-secrets: ${{ steps.check.outputs.has-secrets }}
|
||||
steps:
|
||||
- name: "Check for secrets"
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.SLACK_WEBHOOK_URL != '') || '' }}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
notify:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Download teams.yml to know which label is for which team"
|
||||
run: wget https://raw.githubusercontent.com/grafana/grafana/main/.github/teams.yml
|
||||
|
||||
- name: "Determine which team to notify"
|
||||
run: |
|
||||
# Default to null values.
|
||||
CHANNEL="null"
|
||||
TEAM="null"
|
||||
|
||||
echo "${{ github.event.label.name }} label added"
|
||||
export CURRENT_LABEL="${{ github.event.label.name }}" # Enable the use of the label in yq evaluations
|
||||
# yq is installed by default in ubuntu-latest
|
||||
if [[ $(yq e 'keys | .[] | select(. == env(CURRENT_LABEL))' teams.yml ) ]]; then
|
||||
# Check if we have a channel set to notify on comments.
|
||||
if [[ $(yq '.[env(CURRENT_LABEL)] | has("channel-label")' teams.yml ) == true ]]; then
|
||||
CHANNEL=$(yq '.[env(CURRENT_LABEL)].channel-label' teams.yml)
|
||||
echo "Ready to send issue to channel ID ${CHANNEL}"
|
||||
fi
|
||||
|
||||
if [[ $(yq '.[env(CURRENT_LABEL)] | has("exclude-github-team")' teams.yml ) == true ]]; then
|
||||
TEAM=$(yq '.[env(CURRENT_LABEL)].exclude-github-team' teams.yml)
|
||||
echo "Will not send issue to channel if issue author is part of the team ${TEAM}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# set environment for next steps
|
||||
echo "CHANNEL=${CHANNEL}" >> "$GITHUB_ENV"
|
||||
echo "TEAM=${TEAM}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: "Prepare payload"
|
||||
uses: frabert/replace-string-action@v2.5
|
||||
id: preparePayload
|
||||
with:
|
||||
# replace double quotes with single quotes to avoid breaking the JSON payload sent to Slack
|
||||
string: ${{ github.event.issue.title }}
|
||||
pattern: '"'
|
||||
replace-with: "'"
|
||||
flags: 'g'
|
||||
|
||||
- name: Get Token
|
||||
id: get_workflow_token
|
||||
uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a
|
||||
with:
|
||||
app_id: ${{ secrets.APP_GRAFANA_TEAM_CHECKER_ID }}
|
||||
private_key: ${{ secrets.APP_GRAFANA_TEAM_CHECKER_KEY }}
|
||||
|
||||
- name: "Check that issue author is not part of the team"
|
||||
if: ${{ env.TEAM != 'null' }}
|
||||
run: |
|
||||
response=$(gh api /orgs/grafana/teams/${{ env.TEAM }}/memberships/${{ github.event.issue.user.login }} -i -H "Accept: application/vnd.github.v3+json")
|
||||
STATUS_CODE=$(echo "$response" | head -n 1 | cut -d' ' -f2)
|
||||
if [ "$STATUS_CODE" -eq "404" ]; then
|
||||
echo "The user was not found in the team."
|
||||
echo "USER_FOUND=false" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "The user was potentially found in the team"
|
||||
echo "USER_FOUND=maybe" >> "$GITHUB_ENV"
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get_workflow_token.outputs.token }}
|
||||
|
||||
- name: "Send Slack notification"
|
||||
if: ${{ (env.CHANNEL != 'null') && ((env.USER_FOUND == 'false') || (env.TEAM != 'null')) }}
|
||||
uses: slackapi/slack-github-action@v1.26.0
|
||||
with:
|
||||
payload: >
|
||||
{
|
||||
"icon_emoji": ":grafana:",
|
||||
"username": "Grafana issue labeled",
|
||||
"text": "Issue \"${{ steps.preparePayload.outputs.replaced }}\" labeled \"${{ github.event.label.name }}\": ${{ github.event.issue.html_url }}, please triage.",
|
||||
"channel": "${{ env.CHANNEL }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
+2
-1
@@ -3,4 +3,5 @@
|
||||
List of previous team members that have had a big impact on the company or the product and contributed during a long period of time.
|
||||
|
||||
- Hugo Häggmark ([Björn Lundén](https://www.bjornlunden.se/))
|
||||
- Marcus Efraimsson
|
||||
- [Marcus Efraimsson](https://github.com/marefr)
|
||||
- [Giordano Ricci](https://github.com/elfo404)
|
||||
@@ -50,6 +50,7 @@ require (
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 // indirect
|
||||
@@ -60,7 +61,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.26.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
|
||||
@@ -106,8 +106,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -143,8 +143,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
+29
-4
@@ -56,8 +56,6 @@ This section describes how to:
|
||||
|
||||
In both cases, the assignment applies only to the user, team or service account within the affected organization, and no other organizations. For example, if you grant the user the **Data source editor** role in the **Main** organization, then the user can edit data sources in the **Main** organization, but not in other organizations.
|
||||
|
||||
<br/>
|
||||
|
||||
**Before you begin:**
|
||||
|
||||
- [Plan your RBAC rollout strategy](ref:plan-rbac-rollout-strategy).
|
||||
@@ -71,8 +69,6 @@ In both cases, the assignment applies only to the user, team or service account
|
||||
- Your Grafana user can also assign fixed role if it has either the `fixed:roles:writer` fixed role assigned to the same organization to which you are assigning RBAC to a user, or a custom role with `users.roles:add` and `users.roles:remove` permissions.
|
||||
- Your own user account must have the roles you are granting. For example, if you would like to grant the `fixed:users:writer` role to a team, you must have that role yourself.
|
||||
|
||||
<br/>
|
||||
|
||||
**To assign a fixed role to a user, team or service account:**
|
||||
|
||||
1. Sign in to Grafana.
|
||||
@@ -93,6 +89,35 @@ In both cases, the assignment applies only to the user, team or service account
|
||||
1. Select a role within an organization that you want to assign to the user.
|
||||
1. Click **Save**.
|
||||
|
||||
## Assign fixed or custom roles using the HTTP API
|
||||
|
||||
This section describes how to:
|
||||
|
||||
- Assign a fixed role to a user, team or service account as an organization administrator.
|
||||
|
||||
**Before you begin:**
|
||||
|
||||
- [Plan your RBAC rollout strategy](ref:plan-rbac-rollout-strategy).
|
||||
- Identify the fixed roles that you want to assign to the user, team or service account.
|
||||
|
||||
For more information about available fixed roles, refer to [RBAC role definitions](ref:rbac-role-definitions).
|
||||
|
||||
- Ensure that your own user account has the correct permissions:
|
||||
- If you are assigning permissions to a user, team or service account within an organization, you must have organization administrator or server administrator permissions.
|
||||
- If you are assigning permissions to a user who belongs to multiple organizations, you must have server administrator permissions.
|
||||
- Your Grafana user can also assign fixed role if it has either the `fixed:roles:writer` fixed role assigned to the same organization to which you are assigning RBAC to a user, or a custom role with `users.roles:add` and `users.roles:remove` permissions.
|
||||
- Your own user account must have the roles you are granting. For example, if you would like to grant the `fixed:users:writer` role to a team, you must have that role yourself.
|
||||
|
||||
**To assign a fixed role to a user or service account:**
|
||||
|
||||
Refer to the [RBAC HTTP API for user role assignment](/docs/grafana/<GRAFANA_VERSION>/developers/http_api/access_control/#add-a-user-role-assignment).
|
||||
|
||||
The service account ID can be used in place of the user ID.
|
||||
|
||||
**To assign a fixed role to a team:**
|
||||
|
||||
Refer to the [RBAC HTTP API for team role assignment](/docs/grafana/<GRAFANA_VERSION>/developers/http_api/access_control/#add-a-team-role-assignment).
|
||||
|
||||
## Assign fixed or custom roles to a team using provisioning
|
||||
|
||||
Instead of using the Grafana role picker, you can use file-based provisioning to assign fixed roles to teams. If you have a large number of teams, provisioning can provide an easier approach to assigning and managing role assignments.
|
||||
|
||||
@@ -161,8 +161,8 @@ By default, service account tokens don't have an expiration date, meaning they w
|
||||
|
||||
## Assign roles to a service account in Grafana
|
||||
|
||||
You can assign roles to a Grafana service account to control access for the associated service account tokens.
|
||||
You can assign roles to a service account using the Grafana UI or via the API. For more information about assigning a role to a service account via the API, refer to [Update service account using the HTTP API](ref:api-update-service-account).
|
||||
You can assign organization roles (`Viewer`, `Editor`, `Admin`) to a Grafana service account to control access for the associated service account tokens.
|
||||
You can assign organization roles to a service account using the Grafana UI or via the API. For more information about assigning a role to a service account via the API, refer to [Update service account using the HTTP API](ref:api-update-service-account).
|
||||
|
||||
In [Grafana Enterprise](/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/), you can also [assign RBAC roles](ref:rbac-assign-rbac-roles) to grant very specific permissions to applications that interact with Grafana.
|
||||
|
||||
|
||||
@@ -18,16 +18,16 @@ labels:
|
||||
title: Template annotations and labels
|
||||
weight: 500
|
||||
refs:
|
||||
labels:
|
||||
reference-labels:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/annotation-label/#labels
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/templates/reference/#labels
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#labels
|
||||
values:
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templates/reference/#labels
|
||||
reference-values:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/annotation-label/#values
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/templates/reference/#values
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#values
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templates/reference/#values
|
||||
annotations:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/annotation-label/#annotations
|
||||
@@ -92,7 +92,7 @@ Refer to [Templates Introduction](ref:intro-to-templates) for a more detailed ex
|
||||
|
||||
Both types of templates are written in the Go templating system. However, it's important to understand that variables and functions used in notification templates are different from those used in annotation and label templates.
|
||||
|
||||
1. **Template annotations and labels**: These templates add extra information to individual alert instances. Template variables like [`$labels`](ref:labels) and [`$values`](ref:values) represent alert query data of the individual alert instance.
|
||||
1. **Template annotations and labels**: These templates add extra information to individual alert instances. Template variables like [`$labels`](ref:reference-labels) and [`$values`](ref:reference-values) represent alert query data of the individual alert instance.
|
||||
1. **Template notifications**: Notification templates format the notification content for a group of alerts. Variables like [`.Alerts`](ref:notification-data-reference) include all firing and resolved alerts in the notification.
|
||||
|
||||
## Template annotations
|
||||
@@ -111,11 +111,11 @@ CPU usage has exceeded 80% for the last 5 minutes.
|
||||
|
||||
However, if you want to display dynamic query values in annotations, you need to use template code. Common use cases include:
|
||||
|
||||
- Displaying the query value or threshold that triggered the alert.
|
||||
- Highlighting label information that identifies the alert, such as environment, region, or priority.
|
||||
- Displaying the query value that triggered the alert.
|
||||
- Highlighting label information that identifies the alert, such as the environment, instance, or region.
|
||||
- Providing specific instructions based on query values.
|
||||
- Customizing runbook links depending on query or label values.
|
||||
- Including contact information based on alert labels.
|
||||
- Customizing runbook links depending on query labels.
|
||||
- Including contact information based on query labels.
|
||||
|
||||
For instance, you can template the previous example to display the specific instance and CPU value that triggered the alert.
|
||||
|
||||
|
||||
@@ -115,7 +115,9 @@ CPU usage has exceeded 80% (81.2345) for the last 5 minutes.
|
||||
|
||||
### Include labels for extra details
|
||||
|
||||
To provide additional context, you can include labels from the query. For instance, access the [`$labels`](ref:reference-labels) variable to display a label that informs about the affected instance:
|
||||
To provide additional context, you can include labels from the query using the [`$labels`](ref:reference-labels) variable.
|
||||
|
||||
For instance, the previous case could also include the affected `instance`.
|
||||
|
||||
```
|
||||
CPU usage for {{ $labels.instance }} has exceeded 80% ({{ $values.A.Value }}) for the last 5 minutes.
|
||||
@@ -125,14 +127,14 @@ CPU usage for {{ $labels.instance }} has exceeded 80% ({{ $values.A.Value }}) fo
|
||||
CPU usage for Instance 1 has exceeded 80% (81.2345) for the last 5 minutes.
|
||||
```
|
||||
|
||||
Annotations can also be used to provide a summary of key alert labels, such as the environment and alert severity. For instance, you can display a summary of the alert with important labels like:
|
||||
You can incorporate any labels returned by the query into the template. For instance, the following template includes information about the environment and region where the alert occurred.
|
||||
|
||||
```
|
||||
Alert triggered in {{ $labels.environment }} with severity {{ $labels.severity }}
|
||||
Alert triggered in {{ $labels.environment }} on {{ $labels.region }} region.
|
||||
```
|
||||
|
||||
```template_output
|
||||
Alert triggered in production with severity critical.
|
||||
Alert triggered in production on AMER region.
|
||||
```
|
||||
|
||||
### Print a range query
|
||||
|
||||
@@ -16,6 +16,11 @@ title: Annotation and label template reference
|
||||
menuTitle: Template reference
|
||||
weight: 101
|
||||
refs:
|
||||
label-types:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/annotation-label/#label-types
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#label-types
|
||||
notification-template-reference:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/template-notifications/reference/
|
||||
@@ -72,15 +77,15 @@ The following variables are available when templating annotations and labels:
|
||||
|
||||
| Variables | Description |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [$labels](#labels) | Contains all labels from the query. |
|
||||
| [$labels](#labels) | Contains all labels from the query, only query labels. |
|
||||
| [$values](#values) | Contains the labels and floating point values of all instant queries and expressions, indexed by their Ref IDs. |
|
||||
| [$value](#value) | A string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule. |
|
||||
|
||||
### $labels
|
||||
|
||||
The `$labels` variable contains all labels from the query.
|
||||
The `$labels` variable contains all labels from the query. It excludes [user-configured and reserved labels](ref:label-types), containing only query labels.
|
||||
|
||||
{{< figure src="/media/docs/alerting/query-labels-and_value.png" max-width="1200px" caption="An alert rule displaying labels and value from a query" >}}
|
||||
{{< figure src="/media/docs/alerting/query-labels-and-values.png" max-width="1200px" caption="An alert rule displaying labels and value from a query." >}}
|
||||
|
||||
For example, suppose you have a query that returns CPU usage for all of your servers, and you have an alert rule that fires when any of your servers have exceeded 80% CPU usage for the last 5 minutes. You want to add a summary annotation to the alert that tells you which server is experiencing high CPU usage. With the `$labels` variable you can write a template that prints a human-readable sentence such as:
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ title: Notification template reference
|
||||
menuTitle: Template reference
|
||||
weight: 102
|
||||
refs:
|
||||
label-types:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/fundamentals/alert-rules/annotation-label/#label-types
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#label-types
|
||||
alert-rule-template-reference:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/alerting-rules/templates/reference/
|
||||
@@ -88,7 +93,7 @@ Here's an example that prints all available notification data from dot (`.`):
|
||||
| Name | Type | Description |
|
||||
| -------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Status` | string | Firing or resolved. |
|
||||
| `Labels` | [KV](#kv) | The labels for this alert. |
|
||||
| `Labels` | [KV](#kv) | The labels for this alert. It includes all [types of labels](ref:label-types). |
|
||||
| `Annotations` | [KV](#kv) | The annotations for this alert. |
|
||||
| `StartsAt` | [Time](#time) | The time the alert fired |
|
||||
| `EndsAt` | [Time](#time) | Only set if the end time of an alert is known. Otherwise set to a configurable timeout period from the time since the last alert was received. |
|
||||
|
||||
@@ -87,11 +87,13 @@ Labels that you manually configure in the alert rule to identify the generated a
|
||||
|
||||
Additionally, you can use a [template](ref:templates) to customize the label value and generate dynamic values from query data.
|
||||
|
||||
**Data source query labels**
|
||||
**Query labels**
|
||||
|
||||
For example, if you are monitoring temperature readings and each time series for these readings has a `sensor_id`, and a `location` label, an alert instance might have the labels `{sensor_id="1",location="NY"}`, while another alert instance might have `{sensor_id="2",location="WA"}`.
|
||||
Query labels are labels returned by the data source query.
|
||||
|
||||
Data source query labels labels are also used to generate multiple alert instances from the same alert rule, helping to distinguish alerts from different data.
|
||||
{{< figure src="/media/docs/alerting/query-labels-and-values.png" max-width="1200px" caption="An alert rule query returning labels from the query." >}}
|
||||
|
||||
Query labels can generate multiple alert instances from the same alert rule, helping to distinguish alerts from different data. In this example, the `instance` label generates an alert instance for each server.
|
||||
|
||||
**Reserved labels**
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ You can use the **Search** query builder's user interface to select options to s
|
||||
These selections generate a TraceQL query.
|
||||
Any query generated using **Search** query builder can be transferred to the **TraceQL** query editor, where you can edit the query directly.
|
||||
|
||||
To learn more about how to query by TraceQL, refer to the [TraceQL documentation](/docs/tempo/latest/traceql).
|
||||
To learn more about how to query by TraceQL, refer to the [TraceQL documentation](https://grafana.com/docs/tempo/<TEMPO_VERSION>/traceql).
|
||||
|
||||
## Choose a query editing mode
|
||||
|
||||
@@ -123,15 +123,15 @@ Using the service graph view, you can:
|
||||
- Determine how long the slowest queries in your service take to complete.
|
||||
- Examine all traces that contain spans of particular interest based on rate, error, and duration values (RED signals).
|
||||
|
||||
For more information about the service graph, refer to [Service graph](../service-graph/).
|
||||
For more information about the service graph, refer to [Service graph](../service-graph).
|
||||
|
||||

|
||||
|
||||
## Use TraceQL panels in dashboards
|
||||
|
||||
To add TraceQL panels to your dashboard, refer to the [Traces panel documentation](/docs/grafana/latest/panels-visualizations/visualizations/traces/).
|
||||
To add TraceQL panels to your dashboard, refer to the [Traces panel documentation](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/panels-visualizations/visualizations/traces/).
|
||||
|
||||
To learn more about Grafana dashboards, refer to the [Use dashboards documentation](/docs/grafana/latest/dashboards/use-dashboards/).
|
||||
To learn more about Grafana dashboards, refer to the [Use dashboards documentation](/docs/grafana/<GRAFANA_VERSION>/dashboards/use-dashboards/).
|
||||
|
||||
## Set options for query builder and editor
|
||||
|
||||
|
||||
@@ -74,8 +74,8 @@ require (
|
||||
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
|
||||
github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/alerting v0.0.0-20241021123319-be61d61f71e7 // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d // @grafana/identity-access-team
|
||||
github.com/grafana/authlib v0.0.0-20241202090931-5f1dcf611f9c // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 // @grafana/identity-access-team
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d // @grafana/dataviz-squad
|
||||
github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
@@ -89,7 +89,7 @@ require (
|
||||
github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4 // @grafana/plugins-platform-backend
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1 // @grafana/plugins-platform-backend
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad
|
||||
@@ -148,7 +148,7 @@ require (
|
||||
github.com/spf13/cobra v1.8.1 // @grafana/grafana-app-platform-squad
|
||||
github.com/spf13/pflag v1.0.5 // @grafana-app-platform-squad
|
||||
github.com/spyzhov/ajson v0.9.0 // @grafana/grafana-app-platform-squad
|
||||
github.com/stretchr/testify v1.9.0 // @grafana/grafana-backend-group
|
||||
github.com/stretchr/testify v1.10.0 // @grafana/grafana-backend-group
|
||||
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf // @grafana/grafana-backend-group
|
||||
github.com/ua-parser/uap-go v0.0.0-20211112212520-00c877edfe0f // @grafana/grafana-backend-group
|
||||
github.com/urfave/cli v1.22.16 // indirect; @grafana/grafana-backend-group
|
||||
@@ -175,7 +175,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // @grafana/alerting-backend
|
||||
golang.org/x/mod v0.22.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/net v0.31.0 // @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/oauth2 v0.23.0 // @grafana/identity-access-team
|
||||
golang.org/x/oauth2 v0.24.0 // @grafana/identity-access-team
|
||||
golang.org/x/sync v0.9.0 // @grafana/alerting-backend
|
||||
golang.org/x/text v0.20.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/time v0.6.0 // @grafana/grafana-backend-group
|
||||
|
||||
@@ -2292,10 +2292,10 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/alerting v0.0.0-20241021123319-be61d61f71e7 h1:lsM/QscEX+ZDIJm48ynQscH+msETyGYV6ug8L4f2DtM=
|
||||
github.com/grafana/alerting v0.0.0-20241021123319-be61d61f71e7/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
|
||||
github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a h1:X3mroOOXdryRY6SEIQp7S6l1oxt964ywSmcknszHwn0=
|
||||
github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a/go.mod h1:XFhcSCEDeOgV43x41we7mRBcizDpsTScb7XOuYipQZg=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d h1:7nZfaXdC4Xc2ocMz5/Bx/3EsaEO34KsmA2RRcuogLnc=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
|
||||
github.com/grafana/authlib v0.0.0-20241202090931-5f1dcf611f9c h1:10aFnI5iFk0XSw19FD26oS4nIwCunfbkYT800hpHBzo=
|
||||
github.com/grafana/authlib v0.0.0-20241202090931-5f1dcf611f9c/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d h1:hrXbGJ5jgp6yNITzs5o+zXq0V5yT3siNJ+uM8LGwWKk=
|
||||
github.com/grafana/codejen v0.0.4-0.20230321061741-77f656893a3d/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
|
||||
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=
|
||||
@@ -2331,8 +2331,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs=
|
||||
github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4 h1:Qt/Ic+7K7n7z4ncZs29q2OhOOPKWjE5+ms2ckeYlTo4=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4/go.mod h1:BS2B+lEHC4KtOViiS4C5x9SsR1ihPHAzesGOpaPYeCs=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1 h1:KzbooQP9mv/9CPsn+SoUwGuomA8oUxO0iuIq6Rg/ekE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1/go.mod h1:JriieK5Oc5v120QKhMs/LO55N0P3YI2ttEiVT1wfYsw=
|
||||
github.com/grafana/grafana/apps/playlist v0.0.0-20241105090059-facca37f4d1f h1:zZN/Jy7PjoqtrMiBRV5O3x4xAArcSbUznuyAPACrKXI=
|
||||
github.com/grafana/grafana/apps/playlist v0.0.0-20241105090059-facca37f4d1f/go.mod h1:e97Zxn1WX4Wn9TXEvwTjMNwU6yrjX/K7uVNSCZyEwxY=
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI=
|
||||
@@ -3230,8 +3230,9 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
@@ -3746,8 +3747,8 @@ golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -987,6 +987,7 @@ github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18 h1:3cQ+d+fkNL2Eqp
|
||||
github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4=
|
||||
github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10 h1:oDbLKM34O+JUF9EQFS+9aYhdYoeNfUpXqNjFCLIxwF4=
|
||||
github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk=
|
||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I=
|
||||
github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw=
|
||||
github.com/grafana/cog v0.0.4/go.mod h1:lzetOuhGUl/JaSACiJoHvBokf9/fS6PEFaWZvnQu2vs=
|
||||
@@ -1001,6 +1002,8 @@ github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2 h1:8xGrYqQ1GM4aaMk
|
||||
github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU=
|
||||
github.com/grafana/grafana-app-sdk v0.19.0/go.mod h1:y0BgzYxc+a7CwOqkwUhN9zXd5cgZJjd2zAbgHEd/xzo=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1 h1:KzbooQP9mv/9CPsn+SoUwGuomA8oUxO0iuIq6Rg/ekE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1/go.mod h1:JriieK5Oc5v120QKhMs/LO55N0P3YI2ttEiVT1wfYsw=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE=
|
||||
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU=
|
||||
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU=
|
||||
@@ -1444,6 +1447,8 @@ github.com/stoewer/parquet-cli v0.0.7 h1:rhdZODIbyMS3twr4OM3am8BPPT5pbfMcHLH93wh
|
||||
github.com/stoewer/parquet-cli v0.0.7/go.mod h1:bskxHdj8q3H1EmfuCqjViFoeO3NEvs5lzZAQvI8Nfjk=
|
||||
github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo=
|
||||
github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e h1:mOtuXaRAbVZsxAHVdPR3IjfmN8T1h2iczJLynhLybf8=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/substrait-io/substrait-go v0.4.2 h1:buDnjsb3qAqTaNbOR7VKmNgXf4lYQxWEcnSGUWBtmN8=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/tdewolff/minify/v2 v2.12.9 h1:dvn5MtmuQ/DFMwqf5j8QhEVpPX6fi3WGImhv8RUB4zA=
|
||||
@@ -1792,6 +1797,8 @@ golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
@@ -32,6 +32,7 @@ require (
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20240116145035-ef3ab179eed6 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/yalue/merged_fs v1.3.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
|
||||
+2
-2
@@ -67,8 +67,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yalue/merged_fs v1.3.0 h1:qCeh9tMPNy/i8cwDsQTJ5bLr6IRxbs6meakNE5O+wyY=
|
||||
|
||||
+3
-3
@@ -53,9 +53,9 @@
|
||||
"watch": "yarn start -d watch,start core:start --watchTheme",
|
||||
"ci:test-frontend": "yarn run test:ci",
|
||||
"i18n:stats": "node ./scripts/cli/reportI18nStats.mjs",
|
||||
"betterer": "betterer",
|
||||
"betterer": "betterer --tsconfig ./scripts/cli/tsconfig.json",
|
||||
"betterer:json": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/bettererResultsToJson.ts",
|
||||
"betterer:merge": "betterer merge",
|
||||
"betterer:merge": "betterer merge --tsconfig ./scripts/cli/tsconfig.json",
|
||||
"betterer:stats": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/reportBettererStats.ts",
|
||||
"betterer:issues": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/generateBettererIssues.ts",
|
||||
"generate-icons-bundle-cache-file": "node ./scripts/generate-icon-bundle.js",
|
||||
@@ -87,7 +87,7 @@
|
||||
"@playwright/test": "1.48.2",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
|
||||
"@react-types/button": "3.10.1",
|
||||
"@react-types/menu": "3.9.12",
|
||||
"@react-types/menu": "3.9.13",
|
||||
"@react-types/overlays": "3.8.10",
|
||||
"@react-types/shared": "3.25.0",
|
||||
"@rtk-query/codegen-openapi": "^1.2.0",
|
||||
|
||||
@@ -620,7 +620,7 @@ export class PrometheusDatasource
|
||||
// it is used in metric_find_query.ts
|
||||
// and in Tempo here grafana/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx
|
||||
async getTagKeys(options: DataSourceGetTagKeysOptions<PromQuery>): Promise<MetricFindValue[]> {
|
||||
if (config.featureToggles.promQLScope && !!options) {
|
||||
if (config.featureToggles.promQLScope && (options?.scopes?.length ?? 0) > 0) {
|
||||
const suggestions = await this.languageProvider.fetchSuggestions(
|
||||
options.timeRange,
|
||||
options.queries,
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"rc-slider": "11.1.7",
|
||||
"rc-time-picker": "^3.7.3",
|
||||
"rc-tooltip": "6.2.1",
|
||||
"react-calendar": "5.1.0",
|
||||
"react-calendar": "^5.1.0",
|
||||
"react-colorful": "5.6.1",
|
||||
"react-custom-scrollbars-2": "4.5.0",
|
||||
"react-dropzone": "14.3.5",
|
||||
|
||||
@@ -4,11 +4,11 @@ go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/emicklei/go-restful/v3 v3.11.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.opentelemetry.io/otel v1.32.0
|
||||
k8s.io/api v0.31.3
|
||||
k8s.io/apimachinery v0.31.3
|
||||
@@ -138,7 +138,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.26.0 // indirect
|
||||
|
||||
@@ -130,8 +130,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4 h1:Qt/Ic+7K7n7z4ncZs29q2OhOOPKWjE5+ms2ckeYlTo4=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4/go.mod h1:BS2B+lEHC4KtOViiS4C5x9SsR1ihPHAzesGOpaPYeCs=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1 h1:KzbooQP9mv/9CPsn+SoUwGuomA8oUxO0iuIq6Rg/ekE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1/go.mod h1:JriieK5Oc5v120QKhMs/LO55N0P3YI2ttEiVT1wfYsw=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA=
|
||||
@@ -298,8 +298,9 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
@@ -406,8 +407,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
+3
-9
@@ -49,7 +49,6 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
|
||||
folderRoute.Get("/id/:id", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
|
||||
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
|
||||
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
|
||||
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
|
||||
|
||||
@@ -64,6 +63,7 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
|
||||
folderRoute.Post("/", handler.createFolder)
|
||||
folderRoute.Get("/", handler.getFolders)
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Put("/", handler.updateFolder)
|
||||
folderUidRoute.Delete("/", handler.deleteFolder)
|
||||
folderUidRoute.Get("/", handler.getFolder)
|
||||
})
|
||||
@@ -71,17 +71,11 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
|
||||
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
|
||||
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
|
||||
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
|
||||
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
|
||||
})
|
||||
}
|
||||
// Only adding support for some routes with the k8s handler for now. Include the rest here.
|
||||
if false {
|
||||
handler := newFolderK8sHandler(hs)
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Put("/:uid", handler.updateFolder)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -969,7 +963,7 @@ func (fk8s *folderK8sHandler) toDTO(c *contextmodel.ReqContext, fold *folder.Fol
|
||||
UpdatedBy: updater,
|
||||
Updated: fold.Updated,
|
||||
// #TODO version doesn't seem to be used--confirm or set it properly
|
||||
Version: 1,
|
||||
Version: fold.Version,
|
||||
AccessControl: acMetadata,
|
||||
ParentUID: fold.ParentUID,
|
||||
}, nil
|
||||
|
||||
@@ -5,17 +5,24 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
clientrest "k8s.io/client-go/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
conversions "github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -24,6 +31,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
)
|
||||
@@ -510,3 +518,232 @@ func TestFolderGetAPIEndpoint(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockClientConfigProvider struct {
|
||||
host string
|
||||
}
|
||||
|
||||
func (m mockClientConfigProvider) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config {
|
||||
return &clientrest.Config{
|
||||
Host: m.host,
|
||||
}
|
||||
}
|
||||
|
||||
func (m mockClientConfigProvider) DirectlyServeHTTP(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func TestUpdateFolderLegacyAndUnifiedStorage(t *testing.T) {
|
||||
testuser := &user.User{ID: 99, UID: "fdxsqt7t5ryf4a", Login: "testuser"}
|
||||
|
||||
legacyFolder := folder.Folder{
|
||||
UID: "ady4yobv315a8e",
|
||||
Title: "Example folder 226",
|
||||
URL: "/dashboards/f/ady4yobv315a8e/example-folder-226",
|
||||
CreatedBy: 99,
|
||||
CreatedByUID: "fdxsqt7t5ryf4a",
|
||||
Created: time.Date(2024, time.November, 29, 0, 42, 34, 0, time.UTC),
|
||||
UpdatedBy: 99,
|
||||
UpdatedByUID: "fdxsqt7t5ryf4a",
|
||||
Updated: time.Date(2024, time.November, 29, 0, 42, 34, 0, time.UTC),
|
||||
Version: 3,
|
||||
}
|
||||
|
||||
namespacer := func(_ int64) string { return "1" }
|
||||
unifiedStorageFolder, err := conversions.LegacyFolderToUnstructured(&legacyFolder, namespacer)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFolder := dtos.Folder{
|
||||
UID: legacyFolder.UID,
|
||||
OrgID: 0,
|
||||
Title: legacyFolder.Title,
|
||||
URL: legacyFolder.URL,
|
||||
HasACL: false,
|
||||
CanSave: false,
|
||||
CanEdit: true,
|
||||
CanAdmin: false,
|
||||
CanDelete: false,
|
||||
CreatedBy: "testuser",
|
||||
Created: legacyFolder.Created,
|
||||
UpdatedBy: "testuser",
|
||||
Updated: legacyFolder.Updated,
|
||||
Version: legacyFolder.Version,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) {
|
||||
err := json.NewEncoder(w).Encode(unifiedStorageFolder)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
folderApiServerMock := httptest.NewServer(mux)
|
||||
defer folderApiServerMock.Close()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
folderUID string
|
||||
legacyFolder folder.Folder
|
||||
expectedFolder dtos.Folder
|
||||
expectedFolderServiceError error
|
||||
unifiedStorageEnabled bool
|
||||
unifiedStorageMode grafanarest.DualWriterMode
|
||||
expectedCode int
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
description: "Happy Path - Legacy",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: false,
|
||||
},
|
||||
{
|
||||
description: "Happy Path - Unified storage, mode 1",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode1,
|
||||
},
|
||||
{
|
||||
description: "Happy Path - Unified storage, mode 2",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode2,
|
||||
},
|
||||
{
|
||||
description: "Happy Path - Unified storage, mode 3",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode3,
|
||||
},
|
||||
{
|
||||
description: "Happy Path - Unified storage, mode 4",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode4,
|
||||
},
|
||||
{
|
||||
description: "Folder Not Found - Legacy",
|
||||
expectedCode: http.StatusNotFound,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: "notfound",
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: false,
|
||||
expectedFolderServiceError: dashboards.ErrFolderNotFound,
|
||||
},
|
||||
{
|
||||
description: "Folder Not Found - Unified storage, mode 1",
|
||||
expectedCode: http.StatusNotFound,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: "notfound",
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode1,
|
||||
},
|
||||
{
|
||||
description: "Folder Not Found - Unified storage, mode 2",
|
||||
expectedCode: http.StatusNotFound,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: "notfound",
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode2,
|
||||
},
|
||||
{
|
||||
description: "Folder Not Found - Unified storage, mode 3",
|
||||
expectedCode: http.StatusNotFound,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: "notfound",
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode3,
|
||||
},
|
||||
{
|
||||
description: "Folder Not Found - Unified storage, mode 4",
|
||||
expectedCode: http.StatusNotFound,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: "notfound",
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode4,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
setUpRBACGuardian(t)
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
cfg.UnifiedStorage = map[string]setting.UnifiedStorageConfig{
|
||||
folderv0alpha1.RESOURCEGROUP: {
|
||||
DualWriterMode: tc.unifiedStorageMode,
|
||||
},
|
||||
}
|
||||
|
||||
featuresArr := []any{featuremgmt.FlagNestedFolders}
|
||||
if tc.unifiedStorageEnabled {
|
||||
featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFolders)
|
||||
}
|
||||
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = cfg
|
||||
hs.folderService = &foldertest.FakeService{
|
||||
ExpectedFolder: &tc.legacyFolder,
|
||||
ExpectedError: tc.expectedFolderServiceError,
|
||||
}
|
||||
hs.QuotaService = quotatest.New(false, nil)
|
||||
hs.SearchService = &mockSearchService{
|
||||
ExpectedResult: model.HitList{},
|
||||
}
|
||||
hs.userService = &usertest.FakeUserService{
|
||||
ExpectedUser: testuser,
|
||||
}
|
||||
hs.Features = featuremgmt.WithFeatures(
|
||||
featuresArr...,
|
||||
)
|
||||
hs.clientConfigProvider = mockClientConfigProvider{
|
||||
host: folderApiServerMock.URL,
|
||||
}
|
||||
})
|
||||
|
||||
req := server.NewRequest(http.MethodPut, fmt.Sprintf("/api/folders/%s", tc.folderUID), strings.NewReader(`{"title":"new title"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
|
||||
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("ady4yobv315a8e")},
|
||||
}),
|
||||
}})
|
||||
|
||||
res, err := server.Send(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc.expectedCode, res.StatusCode)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
|
||||
if tc.expectedCode == http.StatusOK {
|
||||
body := dtos.Folder{}
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&body))
|
||||
|
||||
//nolint:staticcheck
|
||||
body.ID = 0
|
||||
body.Version = 0
|
||||
tc.expectedFolder.Version = 0
|
||||
require.Equal(t, tc.expectedFolder, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -46,12 +46,16 @@ func (hs *HTTPServer) redirectFromShortURL(c *contextmodel.ReqContext) {
|
||||
|
||||
shortURL, err := hs.ShortURLService.GetShortURLByUID(c.Req.Context(), c.SignedInUser, shortURLUID)
|
||||
if err != nil {
|
||||
// If we didn't get the URL for whatever reason, we redirect to the
|
||||
// main page, otherwise we get into an endless loops of redirects, as
|
||||
// we would try to redirect again.
|
||||
if shorturls.ErrShortURLNotFound.Is(err) {
|
||||
hs.log.Debug("Not redirecting short URL since not found")
|
||||
hs.log.Debug("Not redirecting short URL since not found", "uid", shortURLUID)
|
||||
c.Redirect(hs.Cfg.AppURL, 308)
|
||||
return
|
||||
}
|
||||
|
||||
hs.log.Error("Short URL redirection error", "err", err)
|
||||
c.Redirect(hs.Cfg.AppURL, 307)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -281,7 +281,7 @@ func (hs *HTTPServer) StartEmailVerificaton(c *contextmodel.ReqContext) response
|
||||
return response.Error(http.StatusBadRequest, "Only users can verify their email", nil)
|
||||
}
|
||||
|
||||
if c.SignedInUser.IsEmailVerified() {
|
||||
if c.SignedInUser.GetEmailVerified() {
|
||||
// email is already verified so we don't need to trigger the flow.
|
||||
return response.Respond(http.StatusNotModified, nil)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ module github.com/grafana/grafana/pkg/apimachinery
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d // @grafana/identity-access-team
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/grafana/authlib v0.0.0-20241202090931-5f1dcf611f9c // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 // @grafana/identity-access-team
|
||||
github.com/stretchr/testify v1.10.0
|
||||
k8s.io/apimachinery v0.31.3
|
||||
k8s.io/apiserver v0.31.3
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340
|
||||
|
||||
@@ -28,10 +28,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a h1:X3mroOOXdryRY6SEIQp7S6l1oxt964ywSmcknszHwn0=
|
||||
github.com/grafana/authlib v0.0.0-20241024120339-84cd3a898e8a/go.mod h1:XFhcSCEDeOgV43x41we7mRBcizDpsTScb7XOuYipQZg=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d h1:7nZfaXdC4Xc2ocMz5/Bx/3EsaEO34KsmA2RRcuogLnc=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
|
||||
github.com/grafana/authlib v0.0.0-20241202090931-5f1dcf611f9c h1:10aFnI5iFk0XSw19FD26oS4nIwCunfbkYT800hpHBzo=
|
||||
github.com/grafana/authlib v0.0.0-20241202090931-5f1dcf611f9c/go.mod h1:x7df73G3xuSD35Xv9cjaMLyPJCgM9Z/Wj5ISouoAfiI=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -61,8 +61,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
||||
@@ -14,48 +14,59 @@ type Requester interface {
|
||||
|
||||
// GetIdentityType returns the type for the requester
|
||||
GetIdentityType() claims.IdentityType
|
||||
|
||||
// IsIdentityType returns true if identity type for requester matches any expected identity type
|
||||
IsIdentityType(expected ...claims.IdentityType) bool
|
||||
|
||||
// FIXME: remove and use GetIdentifier instead.
|
||||
// GetRawIdentifier returns only the identifier part of the UID, excluding the type
|
||||
GetRawIdentifier() string
|
||||
// GetInternalID returns only the identifier part of the ID, excluding the type
|
||||
GetInternalID() (int64, error)
|
||||
|
||||
// GetID returns namespaced internalID for the entity
|
||||
// Deprecated: use GetUID instead
|
||||
GetID() string
|
||||
|
||||
// GetEmail returns the email of the active entity.
|
||||
// Can be empty.
|
||||
GetEmail() string
|
||||
// IsEmailVerified returns if email is verified for entity.
|
||||
IsEmailVerified() bool
|
||||
|
||||
// GetInternalID returns only the identifier part of the ID, excluding the type
|
||||
GetInternalID() (int64, error)
|
||||
|
||||
// GetIsGrafanaAdmin returns true if the user is a server admin
|
||||
GetIsGrafanaAdmin() bool
|
||||
|
||||
// FIXME: remove and use GetUsername instead
|
||||
// GetLogin returns the login of the active entity
|
||||
// Can be empty.
|
||||
GetLogin() string
|
||||
|
||||
// GetOrgID returns the ID of the active organization
|
||||
GetOrgID() int64
|
||||
|
||||
// GetOrgRole returns the role of the active entity in the active organization.
|
||||
GetOrgRole() RoleType
|
||||
|
||||
// GetPermissions returns the permissions of the active entity.
|
||||
GetPermissions() map[string][]string
|
||||
|
||||
// GetGlobalPermissions returns the permissions of the active entity that are available across all organizations.
|
||||
GetGlobalPermissions() map[string][]string
|
||||
|
||||
// DEPRECATED: GetTeams returns the teams the entity is a member of.
|
||||
// Retrieve the teams from the team service instead of using this method.
|
||||
GetTeams() []int64
|
||||
|
||||
// DEPRECATED: GetOrgName returns the name of the active organization.
|
||||
// Retrieve the organization name from the organization service instead of using this method.
|
||||
GetOrgName() string
|
||||
|
||||
// GetAuthID returns external id for entity.
|
||||
GetAuthID() string
|
||||
// GetNamespace returns either "*" or the single namespace this requester has access to
|
||||
// An empty value means the implementation has not specified a kubernetes namespace.
|
||||
GetNamespace() string
|
||||
// GetAuthenticatedBy returns the authentication method used to authenticate the entity.
|
||||
GetAuthenticatedBy() string
|
||||
|
||||
// IsAuthenticatedBy returns true if entity was authenticated by any of supplied providers.
|
||||
IsAuthenticatedBy(providers ...string) bool
|
||||
|
||||
// IsNil returns true if the identity is nil
|
||||
// FIXME: remove this method once all services are using an interface
|
||||
IsNil() bool
|
||||
|
||||
@@ -2,12 +2,13 @@ package identity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
authnlib "github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/authlib/claims"
|
||||
)
|
||||
|
||||
var _ Requester = &StaticRequester{}
|
||||
var _ Requester = (*StaticRequester)(nil)
|
||||
|
||||
// StaticRequester allows creating requester values explicitly.
|
||||
// It is helpful in tests!
|
||||
@@ -35,45 +36,63 @@ type StaticRequester struct {
|
||||
CacheKey string
|
||||
}
|
||||
|
||||
// Access implements Requester.
|
||||
func (u *StaticRequester) GetAccess() claims.AccessClaims {
|
||||
return &IDClaimsWrapper{Source: u}
|
||||
// GetID returns typed id for the entity
|
||||
func (u *StaticRequester) GetID() string {
|
||||
return claims.NewTypeID(u.Type, strconv.FormatInt(u.UserID, 10))
|
||||
}
|
||||
|
||||
// Identity implements Requester.
|
||||
func (u *StaticRequester) GetIdentity() claims.IdentityClaims {
|
||||
if u.IDTokenClaims != nil {
|
||||
return authnlib.NewIdentityClaims(*u.IDTokenClaims)
|
||||
}
|
||||
return &IDClaimsWrapper{Source: u}
|
||||
}
|
||||
|
||||
// GetRawIdentifier implements Requester.
|
||||
func (u *StaticRequester) GetUID() string {
|
||||
return fmt.Sprintf("%s:%s", u.Type, u.UserUID)
|
||||
return claims.NewTypeID(u.Type, u.UserUID)
|
||||
}
|
||||
|
||||
// GetRawIdentifier implements Requester.
|
||||
func (u *StaticRequester) GetRawIdentifier() string {
|
||||
func (u *StaticRequester) GetIdentifier() string {
|
||||
return u.UserUID
|
||||
}
|
||||
|
||||
// GetInternalID implements Requester.
|
||||
func (u *StaticRequester) GetInternalID() (int64, error) {
|
||||
return u.UserID, nil
|
||||
}
|
||||
|
||||
// GetIdentityType implements Requester.
|
||||
func (u *StaticRequester) GetIdentityType() claims.IdentityType {
|
||||
return u.Type
|
||||
}
|
||||
|
||||
// IsIdentityType implements Requester.
|
||||
func (u *StaticRequester) GetSubject() string {
|
||||
return claims.NewTypeID(u.Type, strconv.FormatInt(u.UserID, 10))
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetAudience() []string {
|
||||
return []string{fmt.Sprintf("org:%d", u.OrgID)}
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetTokenPermissions() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetTokenDelegatedPermissions() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetEmailVerified() bool {
|
||||
return u.EmailVerified
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetUsername() string {
|
||||
return u.Login
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetRawIdentifier() string {
|
||||
return u.UserUID
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetInternalID() (int64, error) {
|
||||
return u.UserID, nil
|
||||
}
|
||||
|
||||
func (u *StaticRequester) IsIdentityType(expected ...claims.IdentityType) bool {
|
||||
return claims.IsIdentityType(u.GetIdentityType(), expected...)
|
||||
}
|
||||
|
||||
// GetExtra implements Requester.
|
||||
func (u *StaticRequester) GetExtra() map[string][]string {
|
||||
if u.IDToken != "" {
|
||||
return map[string][]string{"id-token": {u.IDToken}}
|
||||
@@ -81,12 +100,10 @@ func (u *StaticRequester) GetExtra() map[string][]string {
|
||||
return map[string][]string{}
|
||||
}
|
||||
|
||||
// GetGroups implements Requester.
|
||||
func (u *StaticRequester) GetGroups() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// GetName implements Requester.
|
||||
func (u *StaticRequester) GetName() string {
|
||||
if u.Name != "" {
|
||||
return u.Name
|
||||
@@ -171,11 +188,6 @@ func (u *StaticRequester) HasUniqueId() bool {
|
||||
return u.UserID > 0
|
||||
}
|
||||
|
||||
// GetID returns typed id for the entity
|
||||
func (u *StaticRequester) GetID() string {
|
||||
return claims.NewTypeID(u.Type, fmt.Sprintf("%d", u.UserID))
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetAuthID() string {
|
||||
return u.AuthID
|
||||
}
|
||||
@@ -202,12 +214,6 @@ func (u *StaticRequester) IsNil() bool {
|
||||
return u == nil
|
||||
}
|
||||
|
||||
// GetEmail returns the email of the active entity
|
||||
// Can be empty.
|
||||
func (u *StaticRequester) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u *StaticRequester) IsEmailVerified() bool {
|
||||
return u.EmailVerified
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
)
|
||||
|
||||
var _ claims.IdentityClaims = &IDClaimsWrapper{}
|
||||
var _ claims.AccessClaims = &IDClaimsWrapper{}
|
||||
|
||||
type IDClaimsWrapper struct {
|
||||
Source Requester
|
||||
}
|
||||
|
||||
func (i *IDClaimsWrapper) IsNil() bool {
|
||||
return i.Source.IsNil()
|
||||
}
|
||||
|
||||
// GetAuthenticatedBy implements claims.IdentityClaims.
|
||||
func (i *IDClaimsWrapper) AuthenticatedBy() string {
|
||||
return i.Source.GetAuthenticatedBy()
|
||||
}
|
||||
|
||||
// GetDisplayName implements claims.IdentityClaims.
|
||||
func (i *IDClaimsWrapper) DisplayName() string {
|
||||
return i.Source.GetName()
|
||||
}
|
||||
|
||||
// GetEmail implements claims.IdentityClaims.
|
||||
func (i *IDClaimsWrapper) Email() string {
|
||||
return i.Source.GetEmail()
|
||||
}
|
||||
|
||||
// GetEmailVerified implements claims.IdentityClaims.
|
||||
func (i *IDClaimsWrapper) EmailVerified() bool {
|
||||
return i.Source.IsEmailVerified()
|
||||
}
|
||||
|
||||
// GetIdentityType implements claims.IdentityClaims.
|
||||
func (i *IDClaimsWrapper) IdentityType() claims.IdentityType {
|
||||
return i.Source.GetIdentityType()
|
||||
}
|
||||
|
||||
// GetRawUID implements claims.IdentityClaims.
|
||||
func (i *IDClaimsWrapper) Identifier() string {
|
||||
return i.Source.GetRawIdentifier()
|
||||
}
|
||||
|
||||
// GetUsername implements claims.IdentityClaims.
|
||||
func (i *IDClaimsWrapper) Username() string {
|
||||
return i.Source.GetLogin()
|
||||
}
|
||||
|
||||
// GetAudience implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) Audience() []string {
|
||||
return []string{fmt.Sprintf("org:%d", i.Source.GetOrgID())}
|
||||
}
|
||||
|
||||
// GetDelegatedPermissions implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) DelegatedPermissions() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// GetExpiry implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) Expiry() *time.Time {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIssuedAt implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) IssuedAt() *time.Time {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIssuer implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) Issuer() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetJTI implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) JTI() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetNamespace implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) Namespace() string {
|
||||
return i.Source.GetNamespace()
|
||||
}
|
||||
|
||||
// GetNotBefore implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) NotBefore() *time.Time {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPermissions implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) Permissions() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// GetScopes implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) Scopes() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// GetSubject implements claims.AccessClaims.
|
||||
func (i *IDClaimsWrapper) Subject() string {
|
||||
return i.Source.GetID()
|
||||
}
|
||||
@@ -4,10 +4,10 @@ go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.32.0
|
||||
go.opentelemetry.io/otel v1.32.0
|
||||
go.opentelemetry.io/otel/trace v1.32.0
|
||||
@@ -79,7 +79,7 @@ require (
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/term v0.26.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
|
||||
@@ -78,8 +78,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d h1:7nZfaXdC4Xc2ocMz5/Bx/3EsaEO34KsmA2RRcuogLnc=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241024115517-d30b00d7666d/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk=
|
||||
github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 h1:ItDcDxUjVLPKja+hogpqgW/kj8LxUL2qscelXIsN1Bs=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1/go.mod h1:DkxMin+qOh1Fgkxfbt+CUfBqqsCQJMG9op8Os/irBPA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
@@ -162,8 +162,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -244,8 +244,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect; @grafana/grafana-backend-group
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect; @grafana/grafana-backend-group
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect; @grafana/grafana-backend-group
|
||||
github.com/stretchr/testify v1.9.0 // @grafana/grafana-backend-group
|
||||
github.com/stretchr/testify v1.10.0 // @grafana/grafana-backend-group
|
||||
github.com/urfave/cli v1.22.16 // @grafana/grafana-backend-group
|
||||
github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect; @grafana/plugins-platform-backend
|
||||
@@ -38,7 +38,7 @@ require (
|
||||
golang.org/x/crypto v0.29.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/mod v0.22.0 // @grafana/grafana-backend-group
|
||||
golang.org/x/net v0.31.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources
|
||||
golang.org/x/oauth2 v0.23.0 // @grafana/identity-access-team
|
||||
golang.org/x/oauth2 v0.24.0 // @grafana/identity-access-team
|
||||
golang.org/x/sync v0.9.0 // indirect; @grafana/alerting-backend
|
||||
golang.org/x/text v0.20.0 // indirect; @grafana/grafana-backend-group
|
||||
golang.org/x/time v0.6.0 // indirect; @grafana/grafana-backend-group
|
||||
|
||||
+4
-3
@@ -212,8 +212,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
|
||||
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
|
||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||
@@ -286,8 +287,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
+2
-2
@@ -4,13 +4,13 @@ go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/common v0.60.1
|
||||
github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.opentelemetry.io/otel v1.32.0
|
||||
go.opentelemetry.io/otel/trace v1.32.0
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e
|
||||
|
||||
+6
-5
@@ -97,8 +97,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b h1:x2HCzk29I0o5pRPfqWP/qwhXaPGlcz8pohq5kO1NZoE=
|
||||
github.com/grafana/dskit v0.0.0-20240905221822-931a021fb06b/go.mod h1:SPLNCARd4xdjCkue0O6hvuoveuS1dGJjDnfxYe405YQ=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4 h1:Qt/Ic+7K7n7z4ncZs29q2OhOOPKWjE5+ms2ckeYlTo4=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.259.4/go.mod h1:BS2B+lEHC4KtOViiS4C5x9SsR1ihPHAzesGOpaPYeCs=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1 h1:KzbooQP9mv/9CPsn+SoUwGuomA8oUxO0iuIq6Rg/ekE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.260.1/go.mod h1:JriieK5Oc5v120QKhMs/LO55N0P3YI2ttEiVT1wfYsw=
|
||||
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
||||
@@ -234,8 +234,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8 h1:aVGB3YnaS/JNfOW3tiHIlmNmTDg618va+eT0mVomgyI=
|
||||
@@ -306,8 +307,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -132,7 +132,7 @@ type rowsWrapper struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (a *dashboardSqlAccess) Namespaces(ctx context.Context) ([]string, error) {
|
||||
func (a *dashboardSqlAccess) GetResourceStats(ctx context.Context, minCount int) ([]resource.ResourceStats, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
@@ -382,10 +382,9 @@ func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, das
|
||||
}
|
||||
|
||||
var userID int64
|
||||
idClaims := user.GetIdentity()
|
||||
if claims.IsIdentityType(idClaims.IdentityType(), claims.TypeUser) {
|
||||
if claims.IsIdentityType(user.GetIdentityType(), claims.TypeUser) {
|
||||
var err error
|
||||
userID, err = identity.UserIdentifier(idClaims.Subject())
|
||||
userID, err = identity.UserIdentifier(user.GetSubject())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
dashboardv1alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
|
||||
dashboardv2alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
@@ -53,10 +52,6 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return nil // no authorizer
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode {
|
||||
return grafanarest.Mode0
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
if err := dashboardinternal.AddToScheme(scheme); err != nil {
|
||||
return err
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -98,11 +97,6 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return dashboard.GetAuthorizer(b.dashboardService, b.log)
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode {
|
||||
// Add required configuration support in order to enable other modes. For an example, see pkg/registry/apis/playlist/register.go
|
||||
return grafanarest.Mode0
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
return dashboardv0alpha1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
dashboardv1alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -93,11 +92,6 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return dashboard.GetAuthorizer(b.dashboardService, b.log)
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode {
|
||||
// Add required configuration support in order to enable other modes. For an example, see pkg/registry/apis/playlist/register.go
|
||||
return grafanarest.Mode0
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
return dashboardv1alpha1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
dashboardv2alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
@@ -93,11 +92,6 @@ func (b *DashboardsAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return dashboard.GetAuthorizer(b.dashboardService, b.log)
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode {
|
||||
// Add required configuration support in order to enable other modes. For an example, see pkg/registry/apis/playlist/register.go
|
||||
return grafanarest.Mode0
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
return dashboardv2alpha1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) (*f
|
||||
if created != nil {
|
||||
// #TODO Fix this time format. The legacy time format seems to be along the lines of time.Now()
|
||||
// which includes a part that represents a fraction of a second. Format should be "2024-09-12T15:37:41.09466+02:00"
|
||||
createdTime = created.Local()
|
||||
createdTime = (*created).UTC()
|
||||
}
|
||||
|
||||
f := &folder.Folder{
|
||||
@@ -122,6 +122,10 @@ func UnstructuredToLegacyFolder(item unstructured.Unstructured, orgID int64) (*f
|
||||
// #TODO figure out about adding version, parents, orgID fields
|
||||
}
|
||||
|
||||
func LegacyFolderToUnstructured(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) {
|
||||
return convertToK8sResource(v, namespacer)
|
||||
}
|
||||
|
||||
func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) {
|
||||
f := &v0alpha1.Folder{
|
||||
TypeMeta: v0alpha1.FolderResourceInfo.TypeMeta(),
|
||||
@@ -208,11 +212,8 @@ func getURL(meta utils.GrafanaMetaAccessor, title string) string {
|
||||
}
|
||||
|
||||
func getCreated(meta utils.GrafanaMetaAccessor) (*time.Time, error) {
|
||||
created, err := meta.GetRepositoryTimestamp()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return created, nil
|
||||
created := meta.GetCreationTimestamp().Time
|
||||
return &created, nil
|
||||
}
|
||||
|
||||
func GetParentTitles(fullPath string) ([]string, error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package folders
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -246,6 +247,15 @@ func (b *FolderAPIBuilder) Validate(ctx context.Context, a admission.Attributes,
|
||||
|
||||
obj := a.GetObject()
|
||||
|
||||
f, ok := obj.(*v0alpha1.Folder)
|
||||
if !ok {
|
||||
return fmt.Errorf("obj is not v0alpha1.Folder")
|
||||
}
|
||||
|
||||
if f.Spec.Title == "" {
|
||||
return dashboards.ErrFolderTitleEmpty
|
||||
}
|
||||
|
||||
for i := 1; i <= folderValidationRules.maxDepth; i++ {
|
||||
parent := getParent(obj)
|
||||
if parent == "" {
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
@@ -149,6 +148,42 @@ func TestFolderAPIBuilder_getAuthorizerFunc(t *testing.T) {
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user with write permissions should be able to update a folder",
|
||||
input: input{
|
||||
user: &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: orgID,
|
||||
Name: "123",
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgID: {dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersAll}},
|
||||
},
|
||||
},
|
||||
verb: string(utils.VerbUpdate),
|
||||
},
|
||||
expect: expect{
|
||||
eval: "folders:write",
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user without write permissions should NOT be able to update a folder",
|
||||
input: input{
|
||||
user: &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: orgID,
|
||||
Name: "123",
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgID: {},
|
||||
},
|
||||
},
|
||||
verb: string(utils.VerbUpdate),
|
||||
},
|
||||
expect: expect{
|
||||
eval: "folders:write",
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b := &FolderAPIBuilder{
|
||||
@@ -177,9 +212,19 @@ func TestFolderAPIBuilder_getAuthorizerFunc(t *testing.T) {
|
||||
|
||||
func TestFolderAPIBuilder_Validate(t *testing.T) {
|
||||
type input struct {
|
||||
obj *unstructured.Unstructured
|
||||
name string
|
||||
obj *v0alpha1.Folder
|
||||
annotations map[string]string
|
||||
name string
|
||||
}
|
||||
|
||||
circularObj := &v0alpha1.Folder{
|
||||
Spec: v0alpha1.Spec{
|
||||
Title: "foo",
|
||||
},
|
||||
}
|
||||
circularObj.Name = "valid-name"
|
||||
circularObj.Annotations = map[string]string{"grafana.app/folder": "valid-name"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input input
|
||||
@@ -189,9 +234,9 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
|
||||
{
|
||||
name: "should return error when name is invalid",
|
||||
input: input{
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"meta": map[string]interface{}{"name": folderValidationRules.invalidNames[0]},
|
||||
obj: &v0alpha1.Folder{
|
||||
Spec: v0alpha1.Spec{
|
||||
Title: "foo",
|
||||
},
|
||||
},
|
||||
name: folderValidationRules.invalidNames[0],
|
||||
@@ -201,9 +246,9 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
|
||||
{
|
||||
name: "should return no error if every validation passes",
|
||||
input: input{
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"meta": map[string]interface{}{"name": "valid-name"},
|
||||
obj: &v0alpha1.Folder{
|
||||
Spec: v0alpha1.Spec{
|
||||
Title: "foo",
|
||||
},
|
||||
},
|
||||
name: "valid-name",
|
||||
@@ -212,23 +257,33 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
|
||||
{
|
||||
name: "should return error when creating a nested folder higher than max depth",
|
||||
input: input{
|
||||
obj: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"metadata": map[string]any{"name": "valid-name", "annotations": map[string]any{"grafana.app/folder": "valid-name"}},
|
||||
obj: &v0alpha1.Folder{
|
||||
Spec: v0alpha1.Spec{
|
||||
Title: "foo",
|
||||
},
|
||||
},
|
||||
name: "valid-name",
|
||||
annotations: map[string]string{"grafana.app/folder": "valid-name"},
|
||||
name: "valid-name",
|
||||
},
|
||||
setupFn: func(m *mock.Mock) {
|
||||
m.On("Get", mock.Anything, "valid-name", mock.Anything).Return(
|
||||
&unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"metadata": map[string]any{"name": "valid-name", "annotations": map[string]any{"grafana.app/folder": "valid-name"}},
|
||||
},
|
||||
}, nil)
|
||||
circularObj,
|
||||
nil)
|
||||
},
|
||||
err: folder.ErrMaximumDepthReached,
|
||||
},
|
||||
{
|
||||
name: "should return error when title is empty",
|
||||
input: input{
|
||||
obj: &v0alpha1.Folder{
|
||||
Spec: v0alpha1.Spec{
|
||||
Title: "",
|
||||
},
|
||||
},
|
||||
name: "foo",
|
||||
},
|
||||
err: dashboards.ErrFolderTitleEmpty,
|
||||
},
|
||||
}
|
||||
|
||||
s := (grafanarest.Storage)(nil)
|
||||
@@ -246,6 +301,9 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.input.obj.Name = tt.input.name
|
||||
tt.input.obj.Annotations = tt.input.annotations
|
||||
|
||||
if tt.setupFn != nil {
|
||||
tt.setupFn(m)
|
||||
}
|
||||
@@ -264,7 +322,9 @@ func TestFolderAPIBuilder_Validate(t *testing.T) {
|
||||
&user.SignedInUser{},
|
||||
), nil)
|
||||
|
||||
if tt.err != nil {
|
||||
if tt.err == nil {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorIs(t, err, tt.err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration bui
|
||||
return nil // skip registration unless explicitly added (or all experimental are added)
|
||||
}
|
||||
builder := NewPeakQAPIBuilder()
|
||||
apiregistration.RegisterAPI(NewPeakQAPIBuilder())
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,9 @@ func RegisterAPIService(features featuremgmt.FeatureToggles,
|
||||
tracer tracing.Tracer,
|
||||
legacy service.LegacyDataSourceLookup,
|
||||
) (*QueryAPIBuilder, error) {
|
||||
if !(features.IsEnabledGlobally(featuremgmt.FlagQueryService) ||
|
||||
features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs)) {
|
||||
if !featuremgmt.AnyEnabled(features,
|
||||
featuremgmt.FlagQueryService,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
return nil, nil // skip registration unless explicitly added (or all experimental are added)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func RegisterAPIService(features featuremgmt.FeatureToggles, apiregistration bui
|
||||
}
|
||||
|
||||
builder := NewServiceAPIBuilder()
|
||||
apiregistration.RegisterAPI(NewServiceAPIBuilder())
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
}
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ require go.opentelemetry.io/otel v1.32.0
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
)
|
||||
+2
-2
@@ -4,8 +4,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/spf13/pflag"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
)
|
||||
|
||||
type LoggingOptions struct {
|
||||
logger log.Logger
|
||||
Level string
|
||||
}
|
||||
|
||||
func NewLoggingOptions(logger log.Logger) *LoggingOptions {
|
||||
return &LoggingOptions{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *LoggingOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.StringVar(&o.Level, "grafana.log.level", "debug", "Log level, debug, info, warn, error.")
|
||||
}
|
||||
|
||||
func (o *LoggingOptions) Validate() []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *LoggingOptions) ApplyTo(c *genericapiserver.RecommendedConfig) error {
|
||||
err := log.SetupConsoleLogger(o.Level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.logger.Info("Starting grafana-apiserver", "version", setting.BuildVersion, "commit", setting.BuildCommit, "branch", setting.BuildBranch, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
o.logger.Debug("Console logging initialized", "logLevel", o.Level)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/spf13/pflag"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
)
|
||||
|
||||
type MetricsOptions struct {
|
||||
logger log.Logger
|
||||
Enabled bool
|
||||
MetricsRegisterer prometheus.Registerer
|
||||
}
|
||||
|
||||
func NewMetricsOptions(logger log.Logger) *MetricsOptions {
|
||||
return &MetricsOptions{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *MetricsOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.BoolVar(&o.Enabled, "grafana.metrics.enable", false, "Enable metrics and Prometheus /metrics endpoint.")
|
||||
}
|
||||
|
||||
func (o *MetricsOptions) Validate() []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *MetricsOptions) ApplyTo(c *genericapiserver.RecommendedConfig) error {
|
||||
c.EnableMetrics = o.Enabled
|
||||
o.MetricsRegisterer = metrics.ProvideRegisterer()
|
||||
metrics.SetBuildInformation(o.MetricsRegisterer, setting.BuildVersion, setting.BuildCommit, setting.BuildBranch, setting.BuildStamp)
|
||||
|
||||
if o.Enabled {
|
||||
o.logger.Debug("Metrics enabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
genericoptions "k8s.io/apiserver/pkg/server/options"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
LoggingOptions *LoggingOptions
|
||||
ExtraOptions *options.ExtraOptions
|
||||
RecommendedOptions *genericoptions.RecommendedOptions
|
||||
TracingOptions *TracingOptions
|
||||
MetricsOptions *MetricsOptions
|
||||
ProfilingOptions *ProfilingOptions
|
||||
ServerRunOptions *genericoptions.ServerRunOptions
|
||||
StorageOptions *options.StorageOptions
|
||||
}
|
||||
|
||||
func New(logger log.Logger, codec runtime.Codec) *Options {
|
||||
return &Options{
|
||||
LoggingOptions: NewLoggingOptions(logger),
|
||||
ExtraOptions: options.NewExtraOptions(),
|
||||
RecommendedOptions: options.NewRecommendedOptions(codec),
|
||||
TracingOptions: NewTracingOptions(logger),
|
||||
MetricsOptions: NewMetricsOptions(logger),
|
||||
ProfilingOptions: NewProfilingOptions(logger),
|
||||
ServerRunOptions: genericoptions.NewServerRunOptions(),
|
||||
StorageOptions: options.NewStorageOptions(),
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Options) AddFlags(fs *pflag.FlagSet) {
|
||||
o.LoggingOptions.AddFlags(fs)
|
||||
o.ExtraOptions.AddFlags(fs)
|
||||
o.RecommendedOptions.AddFlags(fs)
|
||||
o.TracingOptions.AddFlags(fs)
|
||||
o.MetricsOptions.AddFlags(fs)
|
||||
o.ProfilingOptions.AddFlags(fs)
|
||||
o.ServerRunOptions.AddUniversalFlags(fs)
|
||||
o.internalFlags(fs)
|
||||
}
|
||||
|
||||
func (o *Options) Validate() []error {
|
||||
if errs := o.LoggingOptions.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if errs := o.ExtraOptions.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if errs := o.TracingOptions.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if errs := o.MetricsOptions.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if errs := o.ProfilingOptions.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if errs := o.ServerRunOptions.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if errs := o.internalFlagsValidate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
// NOTE: we don't call validate on the top level recommended options as it doesn't like skipping etcd-servers
|
||||
// the function is left here for troubleshooting any other config issues
|
||||
// errors = append(errors, o.RecommendedOptions.Validate()...)
|
||||
|
||||
if errs := o.RecommendedOptions.SecureServing.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
if o.ExtraOptions.DevMode {
|
||||
// NOTE: Only consider authn for dev mode - resolves the failure due to missing extension apiserver auth-config
|
||||
// in parent k8s
|
||||
if errs := o.RecommendedOptions.Authentication.Validate(); len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A copy of ApplyTo in recommended.go, but for >= 0.28, server pkg in apiserver does a bit extra causing
|
||||
// a panic when CoreAPI is set to nil
|
||||
func (o *Options) ModifiedApplyTo(config *genericapiserver.RecommendedConfig) error {
|
||||
if err := o.RecommendedOptions.Etcd.ApplyTo(&config.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.RecommendedOptions.EgressSelector.ApplyTo(&config.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.RecommendedOptions.Traces.ApplyTo(config.Config.EgressSelector, &config.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.RecommendedOptions.SecureServing.ApplyTo(&config.Config.SecureServing, &config.Config.LoopbackClientConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.RecommendedOptions.Authentication.ApplyTo(&config.Config.Authentication, config.SecureServing, config.OpenAPIConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.RecommendedOptions.Authorization.ApplyTo(&config.Config.Authorization); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := o.RecommendedOptions.Audit.ApplyTo(&config.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: determine whether we need flow control (API priority and fairness)
|
||||
// We can't assume that a shared informers config was provided in standalone mode and will need a guard
|
||||
// when enabling below
|
||||
/* kubeClient, err := kubernetes.NewForConfig(config.ClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := o.RecommendedOptions.Features.ApplyTo(&config.Config, kubeClient, config.SharedInformerFactory); err != nil {
|
||||
return err
|
||||
} */
|
||||
|
||||
if err := o.RecommendedOptions.CoreAPI.ApplyTo(config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := o.RecommendedOptions.ExtraAdmissionInitializers(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := o.ServerRunOptions.ApplyTo(&config.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Options) ApplyTo(serverConfig *genericapiserver.RecommendedConfig) error {
|
||||
if o.LoggingOptions != nil {
|
||||
if err := o.LoggingOptions.ApplyTo(serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if o.ExtraOptions != nil {
|
||||
if err := o.ExtraOptions.ApplyTo(serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if o.RecommendedOptions.CoreAPI == nil {
|
||||
if err := o.ModifiedApplyTo(serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := o.RecommendedOptions.ApplyTo(serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if o.TracingOptions != nil {
|
||||
if err := o.TracingOptions.ApplyTo(serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if o.MetricsOptions != nil {
|
||||
if err := o.MetricsOptions.ApplyTo(serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if o.ProfilingOptions != nil {
|
||||
if err := o.ProfilingOptions.ApplyTo(serverConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if o.ServerRunOptions != nil {
|
||||
if err := o.ServerRunOptions.ApplyTo(&serverConfig.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Options) internalFlags(fs *pflag.FlagSet) {
|
||||
// We also want to be able to set the MaxRequestSize by using flags. This is
|
||||
// usually not exposed by k8s. The value is already set to the upstream default
|
||||
// at this stage, so we can use it as the default.
|
||||
// Reference: https://github.com/kubernetes/kubernetes/blob/v1.31.1/staging/src/k8s.io/apiserver/pkg/server/config.go#L453
|
||||
fs.Int64Var(&o.ServerRunOptions.MaxRequestBodyBytes, "max-request-body-bytes",
|
||||
o.ServerRunOptions.MaxRequestBodyBytes, ""+
|
||||
"Specifies the maximum allowable size for a request payload sent to the API server in bytes."+
|
||||
"The default is 3MB.")
|
||||
}
|
||||
|
||||
func (o *Options) internalFlagsValidate() []error {
|
||||
if o.ServerRunOptions.MaxRequestBodyBytes < 0 {
|
||||
return []error{fmt.Errorf("--max-request-body-bytes can not be a negative value")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/spf13/pflag"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
)
|
||||
|
||||
type ProfilingOptions struct {
|
||||
logger log.Logger
|
||||
blockProfilingRate int
|
||||
mutexProfilingRate int
|
||||
}
|
||||
|
||||
func NewProfilingOptions(logger log.Logger) *ProfilingOptions {
|
||||
return &ProfilingOptions{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *ProfilingOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.IntVar(&o.blockProfilingRate, "grafana.profiling.block-rate", 0, "Controls the fraction of goroutine blocking events that are reported in the blocking profile. The profiler aims to sample an average of one blocking event per rate nanoseconds spent blocked. To turn off profiling entirely, use 0.")
|
||||
fs.IntVar(&o.mutexProfilingRate, "grafana.profiling.mutex-rate", 0, "Controls the fraction of mutex contention events that are reported in the mutex profile. On average 1/rate events are reported. To turn off mutex profiling entirely, use 0.")
|
||||
}
|
||||
|
||||
func (o *ProfilingOptions) Validate() []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *ProfilingOptions) ApplyTo(config *genericapiserver.RecommendedConfig) error {
|
||||
if !config.EnableProfiling {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We bring our own block/mutex profiling configuration
|
||||
config.EnableContentionProfiling = false
|
||||
|
||||
runtime.SetBlockProfileRate(o.blockProfilingRate)
|
||||
runtime.SetMutexProfileFraction(o.mutexProfilingRate)
|
||||
|
||||
o.logger.Info("Profiling enabled", "blockProfileRate", o.blockProfilingRate, "mutexProfileRate", o.mutexProfilingRate)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package options
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/spf13/pflag"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
k8stracing "k8s.io/component-base/tracing"
|
||||
)
|
||||
|
||||
type TracingOptions struct {
|
||||
logger log.Logger
|
||||
|
||||
JaegerAddress string
|
||||
JaegerPropagation string
|
||||
|
||||
OTLPAddress string
|
||||
OTLPPropagation string
|
||||
|
||||
ServiceName string
|
||||
Tags map[string]string
|
||||
|
||||
SamplerType string
|
||||
SamplerParam float64
|
||||
SamplingServiceURL string
|
||||
|
||||
TracingService *tracing.TracingService
|
||||
}
|
||||
|
||||
func NewTracingOptions(logger log.Logger) *TracingOptions {
|
||||
return &TracingOptions{
|
||||
logger: logger,
|
||||
Tags: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (o *TracingOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.StringVar(&o.JaegerAddress, "grafana.tracing.jaeger.address", "", "Tracing Jaeger exporter destination, e.g. http://localhost:14268/api/traces. This enabled the Jaeger export and takes presedence over grafana.tracing.otlp.")
|
||||
fs.StringVar(&o.JaegerPropagation, "grafana.tracing.jaeger.propagation", "jaeger", "Tracing Jaeger propagation specifies the text map propagation format, w3c or jaeger.")
|
||||
fs.StringVar(&o.OTLPAddress, "grafana.tracing.otlp.address", "", "Tracing OTLP exporter destination, e.g. localhost:4317.")
|
||||
fs.StringVar(&o.OTLPPropagation, "grafana.tracing.otlp.propagation", "w3c", "Tracing OTLP propagation specifies the text map propagation format, w3c or jaeger.")
|
||||
fs.StringVar(&o.ServiceName, "grafana.tracing.service-name", "grafana-apiserver", "Override the default service name, grafana-apiserver.")
|
||||
fs.StringToStringVar(&o.Tags, "grafana.tracing.tag", map[string]string{}, "Tracing server tag in 'key=value' format. Specify multiple times to add many.")
|
||||
fs.StringVar(&o.SamplerType, "grafana.tracing.sampler-type", "const", "Tracing sampler type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote.")
|
||||
fs.Float64Var(&o.SamplerParam, "grafana.tracing.sampler-param", 0, "Tracing sampler configuration parameter. For 'const' sampler, 0 or 1 for always false/true respectively. For 'rateLimiting' sampler, the number of spans per second. For 'remote' sampler, param is the same as for 'probabilistic' and indicates the initial sampling rate before the actual one is received from the sampling service.")
|
||||
fs.StringVar(&o.SamplingServiceURL, "grafana.tracing.sampling-service", "", "Tracing server sampling service URL (used for both Jaeger and OTLP) if grafana.tracing.sampler-type=remote.")
|
||||
}
|
||||
|
||||
func (o *TracingOptions) Validate() []error {
|
||||
errs := []error{}
|
||||
|
||||
if o.JaegerAddress != "" {
|
||||
if _, err := url.Parse(o.JaegerAddress); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to parse tracing.jaeger.address: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if o.SamplingServiceURL != "" {
|
||||
if _, err := url.Parse(o.SamplingServiceURL); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to parse tracing.sampling-service: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if o.ServiceName == "" {
|
||||
errs = append(errs, errors.New("grafana.tracing.service-name cannot be empty"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (o *TracingOptions) ApplyTo(config *genericapiserver.RecommendedConfig) error {
|
||||
if err := utilfeature.DefaultMutableFeatureGate.SetFromMap(map[string]bool{
|
||||
string(genericfeatures.APIServerTracing): false,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tracingCfg := tracing.NewEmptyTracingConfig()
|
||||
var err error
|
||||
|
||||
if o.OTLPAddress != "" {
|
||||
tracingCfg, err = tracing.NewOTLPTracingConfig(o.OTLPAddress, o.OTLPPropagation)
|
||||
}
|
||||
|
||||
if o.JaegerAddress != "" {
|
||||
tracingCfg, err = tracing.NewJaegerTracingConfig(o.JaegerAddress, o.JaegerPropagation)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tracingCfg.ServiceName = o.ServiceName
|
||||
tracingCfg.ServiceVersion = setting.BuildVersion
|
||||
|
||||
for k, v := range o.Tags {
|
||||
tracingCfg.CustomAttribs = append(tracingCfg.CustomAttribs, attribute.String(k, v))
|
||||
}
|
||||
|
||||
tracingCfg.Sampler = o.SamplerType
|
||||
tracingCfg.SamplerParam = o.SamplerParam
|
||||
tracingCfg.SamplerRemoteURL = o.SamplingServiceURL
|
||||
tracingCfg.ProfilingIntegration = true
|
||||
|
||||
ts, err := tracing.ProvideService(tracingCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.TracingService = ts
|
||||
config.TracerProvider = ts.GetTracerProvider()
|
||||
|
||||
if config.LoopbackClientConfig != nil {
|
||||
config.LoopbackClientConfig.Wrap(k8stracing.WrapperFor(config.TracerProvider))
|
||||
}
|
||||
|
||||
config.AddPostStartHookOrDie("grafana-tracing-service", func(hookCtx genericapiserver.PostStartHookContext) error {
|
||||
go func() {
|
||||
if err := ts.Run(hookCtx.Context); err != nil && !errors.Is(err, context.Canceled) {
|
||||
o.logger.Error("failed to shutdown tracing service", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
|
||||
|
||||
now := time.Now()
|
||||
idClaims := &auth.IDClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: s.cfg.AppURL,
|
||||
Audience: getAudience(id.GetOrgID()),
|
||||
Subject: id.GetID(),
|
||||
@@ -102,7 +102,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri
|
||||
|
||||
if id.IsIdentityType(claims.TypeUser) {
|
||||
idClaims.Rest.Email = id.GetEmail()
|
||||
idClaims.Rest.EmailVerified = id.IsEmailVerified()
|
||||
idClaims.Rest.EmailVerified = id.GetEmailVerified()
|
||||
idClaims.Rest.AuthenticatedBy = id.GetAuthenticatedBy()
|
||||
idClaims.Rest.Username = id.GetLogin()
|
||||
idClaims.Rest.DisplayName = id.GetName()
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package idtest
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
authnlib "github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/authlib/claims"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
)
|
||||
|
||||
func CreateInternalToken(authInfo claims.AuthInfo, secret []byte) (string, *authnlib.Claims[authnlib.IDTokenClaims], error) {
|
||||
signerOpts := jose.SignerOptions{}
|
||||
signerOpts.WithType("jwt") // Should be uppercase, but this is what authlib expects
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: secret}, &signerOpts)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
identity := authInfo.GetIdentity()
|
||||
now := time.Now()
|
||||
tokenTTL := 10 * time.Minute
|
||||
idClaims := &auth.IDClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Audience: identity.Audience(),
|
||||
Subject: identity.Subject(),
|
||||
Expiry: jwt.NewNumericDate(now.Add(tokenTTL)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
},
|
||||
Rest: authnlib.IDTokenClaims{
|
||||
Namespace: identity.Namespace(),
|
||||
Identifier: identity.Identifier(),
|
||||
Type: identity.IdentityType(),
|
||||
},
|
||||
}
|
||||
|
||||
if claims.IsIdentityType(identity.IdentityType(), claims.TypeUser) {
|
||||
idClaims.Rest.Email = identity.Email()
|
||||
idClaims.Rest.EmailVerified = identity.EmailVerified()
|
||||
idClaims.Rest.AuthenticatedBy = identity.AuthenticatedBy()
|
||||
idClaims.Rest.Username = identity.Username()
|
||||
idClaims.Rest.DisplayName = identity.DisplayName()
|
||||
}
|
||||
|
||||
builder := jwt.Signed(signer).Claims(&idClaims.Rest).Claims(idClaims.Claims)
|
||||
token, err := builder.CompactSerialize()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return token, idClaims, nil
|
||||
}
|
||||
@@ -100,12 +100,12 @@ func (s *ExtendedJWT) authenticateAsUser(
|
||||
accessTokenClaims authlib.Claims[authlib.AccessTokenClaims],
|
||||
) (*authn.Identity, error) {
|
||||
// Only allow id tokens signed for namespace configured for this instance.
|
||||
if allowedNamespace := s.namespaceMapper(s.cfg.DefaultOrgID()); !claims.NamespaceMatches(authlib.NewIdentityClaims(idTokenClaims), allowedNamespace) {
|
||||
if allowedNamespace := s.namespaceMapper(s.cfg.DefaultOrgID()); !claims.NamespaceMatches(idTokenClaims.Rest.Namespace, allowedNamespace) {
|
||||
return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected id token namespace: %s", idTokenClaims.Rest.Namespace)
|
||||
}
|
||||
|
||||
// Allow access tokens with either the same namespace as the validated id token namespace or wildcard (`*`).
|
||||
if !claims.NamespaceMatches(authlib.NewAccessClaims(accessTokenClaims), idTokenClaims.Rest.Namespace) {
|
||||
if !claims.NamespaceMatches(accessTokenClaims.Rest.Namespace, idTokenClaims.Rest.Namespace) {
|
||||
return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.Namespace)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func (s *ExtendedJWT) authenticateAsUser(
|
||||
|
||||
func (s *ExtendedJWT) authenticateAsService(accessTokenClaims authlib.Claims[authlib.AccessTokenClaims]) (*authn.Identity, error) {
|
||||
// Allow access tokens with that has a wildcard namespace or a namespace matching this instance.
|
||||
if allowedNamespace := s.namespaceMapper(s.cfg.DefaultOrgID()); !claims.NamespaceMatches(authlib.NewAccessClaims(accessTokenClaims), allowedNamespace) {
|
||||
if allowedNamespace := s.namespaceMapper(s.cfg.DefaultOrgID()); !claims.NamespaceMatches(accessTokenClaims.Rest.Namespace, allowedNamespace) {
|
||||
return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.Namespace)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ type (
|
||||
|
||||
var (
|
||||
validAccessTokenClaims = accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "access-policy:this-uid",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -40,7 +40,7 @@ var (
|
||||
},
|
||||
}
|
||||
validIDTokenClaims = idTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "user:2",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -51,7 +51,7 @@ var (
|
||||
},
|
||||
}
|
||||
validIDTokenClaimsWithStackSet = idTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "user:2",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -62,7 +62,7 @@ var (
|
||||
},
|
||||
}
|
||||
validIDTokenClaimsWithDeprecatedStackClaimSet = idTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "user:2",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -73,7 +73,7 @@ var (
|
||||
},
|
||||
}
|
||||
validAccessTokenClaimsWildcard = accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "access-policy:this-uid",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -83,7 +83,7 @@ var (
|
||||
},
|
||||
}
|
||||
validAccessTokenClaimsWithStackSet = accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "access-policy:this-uid",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -93,7 +93,7 @@ var (
|
||||
},
|
||||
}
|
||||
validAccessTokenClaimsWithDeprecatedStackClaimSet = accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "access-policy:this-uid",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -103,7 +103,7 @@ var (
|
||||
},
|
||||
}
|
||||
invalidNamespaceIDTokenClaims = idTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "user:2",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -114,7 +114,7 @@ var (
|
||||
},
|
||||
}
|
||||
invalidSubjectIDTokenClaims = idTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "service-account:2",
|
||||
Expiry: jwt.NewNumericDate(time.Date(2023, 5, 3, 0, 0, 0, 0, time.UTC)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Date(2023, 5, 2, 0, 0, 0, 0, time.UTC)),
|
||||
@@ -459,7 +459,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
|
||||
{
|
||||
name: "should return error when the subject is not an access-policy",
|
||||
accessToken: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "user:2",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
@@ -523,7 +523,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "missing iss",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Subject: "access-policy:this-uid",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
ID: "1234567890",
|
||||
@@ -538,7 +538,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "missing expiry",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "access-policy:this-uid",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
@@ -553,7 +553,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "expired token",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "access-policy:this-uid",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
@@ -569,7 +569,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "missing aud",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "access-policy:this-uid",
|
||||
ID: "1234567890",
|
||||
@@ -584,7 +584,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "wrong aud",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "access-policy:this-uid",
|
||||
Audience: jwt.Audience{"http://some-other-host:3000"},
|
||||
@@ -605,7 +605,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "missing sub",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
ID: "1234567890",
|
||||
@@ -620,7 +620,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "missing iat",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "access-policy:this-uid",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
@@ -635,7 +635,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "iat later than current time",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "access-policy:this-uid",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
@@ -651,7 +651,7 @@ func TestVerifyRFC9068TokenFailureScenarios(t *testing.T) {
|
||||
{
|
||||
name: "unsupported alg",
|
||||
payload: &accessTokenClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Claims: jwt.Claims{
|
||||
Issuer: "http://localhost:3000",
|
||||
Subject: "access-policy:this-uid",
|
||||
Audience: jwt.Audience{"http://localhost:3000"},
|
||||
|
||||
@@ -74,32 +74,30 @@ type Identity struct {
|
||||
// Permissions is the list of permissions the entity has.
|
||||
Permissions map[int64]map[string][]string
|
||||
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
|
||||
IDToken string
|
||||
IDTokenClaims *authn.Claims[authn.IDTokenClaims]
|
||||
IDToken string
|
||||
|
||||
IDTokenClaims *authn.Claims[authn.IDTokenClaims]
|
||||
AccessTokenClaims *authn.Claims[authn.AccessTokenClaims]
|
||||
}
|
||||
|
||||
func (i *Identity) GetAccess() claims.AccessClaims {
|
||||
if i.AccessTokenClaims != nil {
|
||||
return authn.NewAccessClaims(*i.AccessTokenClaims)
|
||||
}
|
||||
return &identity.IDClaimsWrapper{Source: i}
|
||||
func (i *Identity) GetID() string {
|
||||
return i.GetSubject()
|
||||
}
|
||||
|
||||
func (i *Identity) GetIdentity() claims.IdentityClaims {
|
||||
if i.IDTokenClaims != nil {
|
||||
return authn.NewIdentityClaims(*i.IDTokenClaims)
|
||||
}
|
||||
return &identity.IDClaimsWrapper{Source: i}
|
||||
func (i *Identity) GetInternalID() (int64, error) {
|
||||
return identity.IntIdentifier(i.GetID())
|
||||
}
|
||||
|
||||
func (i *Identity) GetUID() string {
|
||||
return claims.NewTypeID(i.Type, i.UID)
|
||||
}
|
||||
|
||||
func (i *Identity) GetRawIdentifier() string {
|
||||
return i.UID
|
||||
}
|
||||
|
||||
func (i *Identity) GetInternalID() (int64, error) {
|
||||
return identity.IntIdentifier(i.GetID())
|
||||
func (i *Identity) GetIdentifier() string {
|
||||
return i.UID
|
||||
}
|
||||
|
||||
func (i *Identity) GetIdentityType() claims.IdentityType {
|
||||
@@ -110,6 +108,43 @@ func (i *Identity) IsIdentityType(expected ...claims.IdentityType) bool {
|
||||
return claims.IsIdentityType(i.GetIdentityType(), expected...)
|
||||
}
|
||||
|
||||
func (i *Identity) GetNamespace() string {
|
||||
return i.Namespace
|
||||
}
|
||||
|
||||
func (i *Identity) GetSubject() string {
|
||||
return claims.NewTypeID(i.Type, i.ID)
|
||||
}
|
||||
|
||||
func (i *Identity) GetAudience() []string {
|
||||
if i.AccessTokenClaims != nil {
|
||||
return i.AccessTokenClaims.Audience
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (i *Identity) GetEmailVerified() bool {
|
||||
return i.EmailVerified
|
||||
}
|
||||
|
||||
func (i *Identity) GetTokenPermissions() []string {
|
||||
if i.AccessTokenClaims != nil {
|
||||
return i.AccessTokenClaims.Rest.Permissions
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (i *Identity) GetTokenDelegatedPermissions() []string {
|
||||
if i.AccessTokenClaims != nil {
|
||||
return i.AccessTokenClaims.Rest.DelegatedPermissions
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (i *Identity) GetGroups() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (i *Identity) GetExtra() map[string][]string {
|
||||
extra := map[string][]string{}
|
||||
if i.IDToken != "" {
|
||||
@@ -121,10 +156,6 @@ func (i *Identity) GetExtra() map[string][]string {
|
||||
return extra
|
||||
}
|
||||
|
||||
func (i *Identity) GetGroups() []string {
|
||||
return []string{} // teams?
|
||||
}
|
||||
|
||||
func (i *Identity) GetName() string {
|
||||
if i.Name != "" {
|
||||
return i.Name
|
||||
@@ -135,14 +166,6 @@ func (i *Identity) GetName() string {
|
||||
return i.Email
|
||||
}
|
||||
|
||||
func (i *Identity) GetID() string {
|
||||
return claims.NewTypeID(i.Type, i.ID)
|
||||
}
|
||||
|
||||
func (i *Identity) GetUID() string {
|
||||
return claims.NewTypeID(i.Type, i.UID)
|
||||
}
|
||||
|
||||
func (i *Identity) GetAuthID() string {
|
||||
return i.AuthID
|
||||
}
|
||||
@@ -166,10 +189,6 @@ func (i *Identity) GetEmail() string {
|
||||
return i.Email
|
||||
}
|
||||
|
||||
func (i *Identity) IsEmailVerified() bool {
|
||||
return i.EmailVerified
|
||||
}
|
||||
|
||||
func (i *Identity) GetIDToken() string {
|
||||
return i.IDToken
|
||||
}
|
||||
@@ -178,12 +197,12 @@ func (i *Identity) GetIsGrafanaAdmin() bool {
|
||||
return i.IsGrafanaAdmin != nil && *i.IsGrafanaAdmin
|
||||
}
|
||||
|
||||
func (i *Identity) GetLogin() string {
|
||||
func (i *Identity) GetUsername() string {
|
||||
return i.Login
|
||||
}
|
||||
|
||||
func (i *Identity) GetNamespace() string {
|
||||
return i.Namespace
|
||||
func (i *Identity) GetLogin() string {
|
||||
return i.Login
|
||||
}
|
||||
|
||||
func (i *Identity) GetOrgID() int64 {
|
||||
@@ -263,24 +282,27 @@ func (i *Identity) IsNil() bool {
|
||||
// SignedInUser returns a SignedInUser from the identity.
|
||||
func (i *Identity) SignedInUser() *user.SignedInUser {
|
||||
u := &user.SignedInUser{
|
||||
OrgID: i.OrgID,
|
||||
OrgName: i.OrgName,
|
||||
OrgRole: i.GetOrgRole(),
|
||||
Login: i.Login,
|
||||
Name: i.Name,
|
||||
Email: i.Email,
|
||||
AuthID: i.AuthID,
|
||||
AuthenticatedBy: i.AuthenticatedBy,
|
||||
IsGrafanaAdmin: i.GetIsGrafanaAdmin(),
|
||||
IsAnonymous: i.IsIdentityType(claims.TypeAnonymous),
|
||||
IsDisabled: i.IsDisabled,
|
||||
HelpFlags1: i.HelpFlags1,
|
||||
LastSeenAt: i.LastSeenAt,
|
||||
Teams: i.Teams,
|
||||
Permissions: i.Permissions,
|
||||
IDToken: i.IDToken,
|
||||
FallbackType: i.Type,
|
||||
Namespace: i.Namespace,
|
||||
OrgID: i.OrgID,
|
||||
OrgName: i.OrgName,
|
||||
OrgRole: i.GetOrgRole(),
|
||||
Login: i.Login,
|
||||
Name: i.Name,
|
||||
Email: i.Email,
|
||||
EmailVerified: i.EmailVerified,
|
||||
AuthID: i.AuthID,
|
||||
AuthenticatedBy: i.AuthenticatedBy,
|
||||
Namespace: i.Namespace,
|
||||
IsGrafanaAdmin: i.GetIsGrafanaAdmin(),
|
||||
IsAnonymous: i.IsIdentityType(claims.TypeAnonymous),
|
||||
IsDisabled: i.IsDisabled,
|
||||
HelpFlags1: i.HelpFlags1,
|
||||
LastSeenAt: i.LastSeenAt,
|
||||
Teams: i.Teams,
|
||||
Permissions: i.Permissions,
|
||||
IDToken: i.IDToken,
|
||||
IDTokenClaims: i.IDTokenClaims,
|
||||
AccessTokenClaims: i.AccessTokenClaims,
|
||||
FallbackType: i.Type,
|
||||
}
|
||||
|
||||
if i.IsIdentityType(claims.TypeAPIKey) {
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
GlobalOrgID = int64(0)
|
||||
)
|
||||
|
||||
var _ identity.Requester = &SignedInUser{}
|
||||
var _ identity.Requester = (*SignedInUser)(nil)
|
||||
|
||||
type SignedInUser struct {
|
||||
UserID int64 `xorm:"user_id"`
|
||||
@@ -45,28 +45,28 @@ type SignedInUser struct {
|
||||
Permissions map[int64]map[string][]string `json:"-"`
|
||||
|
||||
// IDToken is a signed token representing the identity that can be forwarded to plugins and external services.
|
||||
IDToken string `json:"-" xorm:"-"`
|
||||
IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims] `json:"-" xorm:"-"`
|
||||
IDToken string `json:"-" xorm:"-"`
|
||||
IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims] `json:"-" xorm:"-"`
|
||||
AccessTokenClaims *authnlib.Claims[authnlib.AccessTokenClaims] `json:"-" xorm:"-"`
|
||||
|
||||
// When other settings are not deterministic, this value is used
|
||||
FallbackType claims.IdentityType
|
||||
}
|
||||
|
||||
// Access implements claims.AuthInfo.
|
||||
func (u *SignedInUser) GetAccess() claims.AccessClaims {
|
||||
return &identity.IDClaimsWrapper{Source: u}
|
||||
func (u *SignedInUser) GetID() string {
|
||||
ns, id := u.getTypeAndID()
|
||||
return claims.NewTypeID(ns, id)
|
||||
}
|
||||
|
||||
// Identity implements claims.AuthInfo.
|
||||
func (u *SignedInUser) GetIdentity() claims.IdentityClaims {
|
||||
if u.IDTokenClaims != nil {
|
||||
return authnlib.NewIdentityClaims(*u.IDTokenClaims)
|
||||
}
|
||||
return &identity.IDClaimsWrapper{Source: u}
|
||||
func (u *SignedInUser) GetInternalID() (int64, error) {
|
||||
return identity.IntIdentifier(u.GetID())
|
||||
}
|
||||
|
||||
// GetRawIdentifier implements Requester.
|
||||
func (u *SignedInUser) GetRawIdentifier() string {
|
||||
func (u *SignedInUser) GetUID() string {
|
||||
return claims.NewTypeID(u.GetIdentityType(), u.GetIdentifier())
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetIdentifier() string {
|
||||
if u.UserUID == "" {
|
||||
// nolint:staticcheck
|
||||
id, _ := u.GetInternalID()
|
||||
@@ -75,12 +75,10 @@ func (u *SignedInUser) GetRawIdentifier() string {
|
||||
return u.UserUID
|
||||
}
|
||||
|
||||
// GetInternalID implements Requester.
|
||||
func (u *SignedInUser) GetInternalID() (int64, error) {
|
||||
return identity.IntIdentifier(u.GetID())
|
||||
func (u *SignedInUser) GetRawIdentifier() string {
|
||||
return u.GetIdentifier()
|
||||
}
|
||||
|
||||
// GetIdentityType implements Requester.
|
||||
func (u *SignedInUser) GetIdentityType() claims.IdentityType {
|
||||
switch {
|
||||
case u.ApiKeyID != 0:
|
||||
@@ -97,7 +95,6 @@ func (u *SignedInUser) GetIdentityType() claims.IdentityType {
|
||||
return u.FallbackType
|
||||
}
|
||||
|
||||
// IsIdentityType implements Requester.
|
||||
func (u *SignedInUser) IsIdentityType(expected ...claims.IdentityType) bool {
|
||||
return claims.IsIdentityType(u.GetIdentityType(), expected...)
|
||||
}
|
||||
@@ -115,7 +112,21 @@ func (u *SignedInUser) GetName() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
// GetExtra implements Requester.
|
||||
func (u *SignedInUser) GetNamespace() string {
|
||||
return u.Namespace
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetSubject() string {
|
||||
return u.GetID()
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetAudience() []string {
|
||||
if u.AccessTokenClaims != nil {
|
||||
return u.AccessTokenClaims.Audience
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetExtra() map[string][]string {
|
||||
extra := map[string][]string{}
|
||||
if u.IDToken != "" {
|
||||
@@ -127,7 +138,6 @@ func (u *SignedInUser) GetExtra() map[string][]string {
|
||||
return extra
|
||||
}
|
||||
|
||||
// GetGroups implements Requester.
|
||||
func (u *SignedInUser) GetGroups() []string {
|
||||
groups := []string{}
|
||||
for _, t := range u.Teams {
|
||||
@@ -136,6 +146,28 @@ func (u *SignedInUser) GetGroups() []string {
|
||||
return groups
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetTokenPermissions() []string {
|
||||
if u.AccessTokenClaims != nil {
|
||||
return u.AccessTokenClaims.Rest.Permissions
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetTokenDelegatedPermissions() []string {
|
||||
if u.AccessTokenClaims != nil {
|
||||
return u.AccessTokenClaims.Rest.DelegatedPermissions
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetEmailVerified() bool {
|
||||
return u.EmailVerified
|
||||
}
|
||||
|
||||
func (u *SignedInUser) ShouldUpdateLastSeenAt() bool {
|
||||
return u.UserID > 0 && time.Since(u.LastSeenAt) > time.Minute*5
|
||||
}
|
||||
@@ -171,10 +203,6 @@ func (u *SignedInUser) HasUniqueId() bool {
|
||||
return u.IsRealUser() || u.IsApiKeyUser() || u.IsServiceAccountUser()
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetNamespace() string {
|
||||
return u.Namespace
|
||||
}
|
||||
|
||||
// GetCacheKey returns a unique key for the entity.
|
||||
// Add an extra prefix to avoid collisions with other caches
|
||||
func (u *SignedInUser) GetCacheKey() string {
|
||||
@@ -193,7 +221,6 @@ func (u *SignedInUser) GetCacheKey() string {
|
||||
return fmt.Sprintf("%d-%s-%s", u.GetOrgID(), typ, id)
|
||||
}
|
||||
|
||||
// GetIsGrafanaAdmin returns true if the user is a server admin
|
||||
func (u *SignedInUser) GetIsGrafanaAdmin() bool {
|
||||
return u.IsGrafanaAdmin
|
||||
}
|
||||
@@ -204,6 +231,11 @@ func (u *SignedInUser) GetLogin() string {
|
||||
return u.Login
|
||||
}
|
||||
|
||||
// GetUsername implements identity.Requester.
|
||||
func (u *SignedInUser) GetUsername() string {
|
||||
return u.Login
|
||||
}
|
||||
|
||||
// GetOrgID returns the ID of the active organization
|
||||
func (u *SignedInUser) GetOrgID() int64 {
|
||||
return u.OrgID
|
||||
@@ -252,12 +284,6 @@ func (u *SignedInUser) GetOrgRole() identity.RoleType {
|
||||
return u.OrgRole
|
||||
}
|
||||
|
||||
// GetID returns namespaced id for the entity
|
||||
func (u *SignedInUser) GetID() string {
|
||||
ns, id := u.getTypeAndID()
|
||||
return claims.NewTypeID(ns, id)
|
||||
}
|
||||
|
||||
func (u *SignedInUser) getTypeAndID() (claims.IdentityType, string) {
|
||||
switch {
|
||||
case u.ApiKeyID != 0:
|
||||
@@ -275,10 +301,6 @@ func (u *SignedInUser) getTypeAndID() (claims.IdentityType, string) {
|
||||
return u.FallbackType, strconv.FormatInt(u.UserID, 10)
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetUID() string {
|
||||
return fmt.Sprintf("%s:%s", u.GetIdentityType(), u.GetRawIdentifier())
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetAuthID() string {
|
||||
return u.AuthID
|
||||
}
|
||||
@@ -301,16 +323,6 @@ func (u *SignedInUser) IsNil() bool {
|
||||
return u == nil
|
||||
}
|
||||
|
||||
// GetEmail returns the email of the active entity
|
||||
// Can be empty.
|
||||
func (u *SignedInUser) GetEmail() string {
|
||||
return u.Email
|
||||
}
|
||||
|
||||
func (u *SignedInUser) IsEmailVerified() bool {
|
||||
return u.EmailVerified
|
||||
}
|
||||
|
||||
func (u *SignedInUser) GetIDToken() string {
|
||||
return u.IDToken
|
||||
}
|
||||
@@ -108,7 +108,8 @@ func (s *cdkBackend) getPath(key *ResourceKey, rv int64) string {
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (s *cdkBackend) Namespaces(ctx context.Context) ([]string, error) {
|
||||
// GetResourceStats implements Backend.
|
||||
func (s *cdkBackend) GetResourceStats(ctx context.Context, minCount int) ([]ResourceStats, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
|
||||
@@ -142,13 +142,12 @@ func idTokenExtractor(ctx context.Context) (string, error) {
|
||||
// If no token is found, create an internal token.
|
||||
// This is a workaround for StaticRequester not having a signed ID token.
|
||||
if staticRequester, ok := authInfo.(*identity.StaticRequester); ok {
|
||||
token, idClaims, err := createInternalToken(staticRequester)
|
||||
token, _, err := createInternalToken(staticRequester)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create internal token: %w", err)
|
||||
}
|
||||
|
||||
staticRequester.IDToken = token
|
||||
staticRequester.IDTokenClaims = idClaims
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@@ -170,29 +169,28 @@ func createInternalToken(authInfo claims.AuthInfo) (string, *authnlib.Claims[aut
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
identity := authInfo.GetIdentity()
|
||||
now := time.Now()
|
||||
tokenTTL := 10 * time.Minute
|
||||
idClaims := &auth.IDClaims{
|
||||
Claims: &jwt.Claims{
|
||||
Audience: identity.Audience(),
|
||||
Subject: identity.Subject(),
|
||||
Claims: jwt.Claims{
|
||||
Audience: authInfo.GetAudience(),
|
||||
Subject: authInfo.GetSubject(),
|
||||
Expiry: jwt.NewNumericDate(now.Add(tokenTTL)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
},
|
||||
Rest: authnlib.IDTokenClaims{
|
||||
Namespace: identity.Namespace(),
|
||||
Identifier: identity.Identifier(),
|
||||
Type: identity.IdentityType(),
|
||||
Namespace: authInfo.GetNamespace(),
|
||||
Identifier: authInfo.GetIdentifier(),
|
||||
Type: authInfo.GetIdentityType(),
|
||||
},
|
||||
}
|
||||
|
||||
if claims.IsIdentityType(identity.IdentityType(), claims.TypeUser) {
|
||||
idClaims.Rest.Email = identity.Email()
|
||||
idClaims.Rest.EmailVerified = identity.EmailVerified()
|
||||
idClaims.Rest.AuthenticatedBy = identity.AuthenticatedBy()
|
||||
idClaims.Rest.Username = identity.Username()
|
||||
idClaims.Rest.DisplayName = identity.DisplayName()
|
||||
if claims.IsIdentityType(authInfo.GetIdentityType(), claims.TypeUser) {
|
||||
idClaims.Rest.Email = authInfo.GetEmail()
|
||||
idClaims.Rest.EmailVerified = authInfo.GetEmailVerified()
|
||||
idClaims.Rest.AuthenticatedBy = authInfo.GetAuthenticatedBy()
|
||||
idClaims.Rest.Username = authInfo.GetUsername()
|
||||
idClaims.Rest.DisplayName = authInfo.GetName()
|
||||
}
|
||||
|
||||
builder := jwt.Signed(signer).Claims(&idClaims.Rest).Claims(idClaims.Claims)
|
||||
|
||||
@@ -81,6 +81,7 @@ type searchSupport struct {
|
||||
access authz.AccessClient
|
||||
builders *builderCache
|
||||
initWorkers int
|
||||
initMinSize int
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -104,6 +105,7 @@ func newSearchSupport(opts SearchOptions, storage StorageBackend, access authz.A
|
||||
search: opts.Backend,
|
||||
log: slog.Default().With("logger", "resource-search"),
|
||||
initWorkers: opts.WorkerThreads,
|
||||
initMinSize: opts.InitMinCount,
|
||||
}
|
||||
|
||||
info, err := opts.Resources.GetDocumentBuilders()
|
||||
@@ -164,40 +166,22 @@ func (s *searchSupport) init(ctx context.Context) error {
|
||||
_, span := s.tracer.Start(ctx, tracingPrexfixSearch+"Init")
|
||||
defer span.End()
|
||||
|
||||
// TODO, replace namespaces with a query that gets top values
|
||||
namespaces, err := s.storage.Namespaces(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Hardcoded for now... should come from the query
|
||||
kinds := []schema.GroupResource{
|
||||
{Group: "dashboard.grafana.app", Resource: "dashboards"},
|
||||
{Group: "playlist.grafana.app", Resource: "playlists"},
|
||||
}
|
||||
|
||||
totalBatchesIndexed := 0
|
||||
group := errgroup.Group{}
|
||||
group.SetLimit(s.initWorkers)
|
||||
|
||||
// Prepare all the (large) indexes
|
||||
// TODO, threading and query real information:
|
||||
// SELECT namespace,"group",resource,COUNT(*),resource_version FROM resource
|
||||
// GROUP BY "group", "resource", "namespace"
|
||||
// ORDER BY resource_version desc;
|
||||
for _, ns := range namespaces {
|
||||
for _, gr := range kinds {
|
||||
group.Go(func() error {
|
||||
s.log.Debug("initializing search index", "namespace", ns, "gr", gr)
|
||||
totalBatchesIndexed++
|
||||
_, _, err = s.build(ctx, NamespacedResource{
|
||||
Group: gr.Group,
|
||||
Resource: gr.Resource,
|
||||
Namespace: ns,
|
||||
}, 10, 0) // TODO, approximate size
|
||||
return err
|
||||
})
|
||||
}
|
||||
stats, err := s.storage.GetResourceStats(ctx, s.initMinSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, info := range stats {
|
||||
group.Go(func() error {
|
||||
s.log.Debug("initializing search index", "namespace", info.Namespace, "group", info.Group, "resource", info.Resource)
|
||||
totalBatchesIndexed++
|
||||
_, _, err = s.build(ctx, info.NamespacedResource, info.Count, info.ResourceVersion)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
err = group.Wait()
|
||||
|
||||
@@ -95,7 +95,14 @@ type StorageBackend interface {
|
||||
// For HA setups, this will be more events than the local WriteEvent above!
|
||||
WatchWriteEvents(ctx context.Context) (<-chan *WrittenEvent, error)
|
||||
|
||||
Namespaces(ctx context.Context) ([]string, error)
|
||||
GetResourceStats(ctx context.Context, minCount int) ([]ResourceStats, error)
|
||||
}
|
||||
|
||||
type ResourceStats struct {
|
||||
NamespacedResource
|
||||
|
||||
Count int64
|
||||
ResourceVersion int64
|
||||
}
|
||||
|
||||
// This interface is not exposed to end users directly
|
||||
@@ -133,6 +140,9 @@ type SearchOptions struct {
|
||||
|
||||
// How many threads should build indexes
|
||||
WorkerThreads int
|
||||
|
||||
// Skip building index on startup for small indexes
|
||||
InitMinCount int
|
||||
}
|
||||
|
||||
type ResourceServerOptions struct {
|
||||
|
||||
@@ -121,6 +121,38 @@ func (b *backend) Stop(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetResourceStats implements Backend.
|
||||
func (b *backend) GetResourceStats(ctx context.Context, minCount int) ([]resource.ResourceStats, error) {
|
||||
_, span := b.tracer.Start(ctx, tracePrefix+".GetResourceStats")
|
||||
defer span.End()
|
||||
|
||||
req := &sqlStatsRequest{
|
||||
SQLTemplate: sqltemplate.New(b.dialect),
|
||||
MinCount: minCount, // not used in query... yet?
|
||||
}
|
||||
|
||||
res := make([]resource.ResourceStats, 0, 100)
|
||||
err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error {
|
||||
rows, err := dbutil.QueryRows(ctx, tx, sqlResourceStats, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
row := resource.ResourceStats{}
|
||||
err = rows.Scan(&row.Namespace, &row.Group, &row.Resource, &row.Count, &row.ResourceVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if row.Count > int64(minCount) {
|
||||
res = append(res, row)
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (b *backend) WriteEvent(ctx context.Context, event resource.WriteEvent) (int64, error) {
|
||||
_, span := b.tracer.Start(ctx, tracePrefix+"WriteEvent")
|
||||
defer span.End()
|
||||
@@ -137,35 +169,6 @@ func (b *backend) WriteEvent(ctx context.Context, event resource.WriteEvent) (in
|
||||
}
|
||||
}
|
||||
|
||||
// Namespaces returns the list of unique namespaces in storage.
|
||||
func (b *backend) Namespaces(ctx context.Context) ([]string, error) {
|
||||
var namespaces []string
|
||||
|
||||
err := b.db.WithTx(ctx, RepeatableRead, func(ctx context.Context, tx db.Tx) error {
|
||||
rows, err := tx.QueryContext(ctx, "SELECT DISTINCT(namespace) FROM resource ORDER BY namespace;")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = rows.Close()
|
||||
}()
|
||||
|
||||
for rows.Next() {
|
||||
var ns string
|
||||
err = rows.Scan(&ns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespaces = append(namespaces, ns)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return namespaces, err
|
||||
}
|
||||
|
||||
func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, error) {
|
||||
ctx, span := b.tracer.Start(ctx, tracePrefix+"Create")
|
||||
defer span.End()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
SELECT
|
||||
{{ .Ident "namespace" }},
|
||||
{{ .Ident "group" }},
|
||||
{{ .Ident "resource" }},
|
||||
COUNT(*),
|
||||
MAX({{ .Ident "resource_version" }})
|
||||
FROM {{ .Ident "resource" }}
|
||||
GROUP BY
|
||||
{{ .Ident "namespace" }},
|
||||
{{ .Ident "group" }},
|
||||
{{ .Ident "resource" }}
|
||||
;
|
||||
@@ -25,6 +25,10 @@ func getEngineMySQL(getter confGetter) (*xorm.Engine, error) {
|
||||
// See: https://dev.mysql.com/doc/refman/en/sql-mode.html
|
||||
"@@SESSION.sql_mode": "ANSI",
|
||||
}
|
||||
sslMode := getter.String("ssl_mode")
|
||||
if sslMode == "true" || sslMode == "skip-verify" {
|
||||
config.Params["tls"] = "preferred"
|
||||
}
|
||||
tls := getter.String("tls")
|
||||
if tls != "" {
|
||||
config.Params["tls"] = tls
|
||||
|
||||
@@ -31,6 +31,7 @@ var (
|
||||
sqlResourceInsert = mustTemplate("resource_insert.sql")
|
||||
sqlResourceUpdate = mustTemplate("resource_update.sql")
|
||||
sqlResourceRead = mustTemplate("resource_read.sql")
|
||||
sqlResourceStats = mustTemplate("resource_stats.sql")
|
||||
sqlResourceList = mustTemplate("resource_list.sql")
|
||||
sqlResourceHistoryList = mustTemplate("resource_history_list.sql")
|
||||
sqlResourceUpdateRV = mustTemplate("resource_update_rv.sql")
|
||||
@@ -71,6 +72,15 @@ func (r sqlResourceRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type sqlStatsRequest struct {
|
||||
sqltemplate.SQLTemplate
|
||||
MinCount int
|
||||
}
|
||||
|
||||
func (r sqlStatsRequest) Validate() error {
|
||||
return nil // TODO
|
||||
}
|
||||
|
||||
type historyPollResponse struct {
|
||||
Key resource.ResourceKey
|
||||
ResourceVersion int64
|
||||
|
||||
@@ -219,5 +219,15 @@ func TestUnifiedStorageQueries(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
sqlResourceStats: {
|
||||
{
|
||||
Name: "query",
|
||||
Data: &sqlStatsRequest{
|
||||
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||
MinCount: 10, // Not yet used in query (only response filter)
|
||||
},
|
||||
},
|
||||
},
|
||||
}})
|
||||
}
|
||||
@@ -65,6 +65,7 @@ func NewResourceServer(ctx context.Context, db infraDB.DB, cfg *setting.Cfg,
|
||||
}, tracer, reg),
|
||||
Resources: docs,
|
||||
WorkerThreads: 5, // from cfg?
|
||||
InitMinCount: 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/grafana/dskit/services"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
||||
@@ -98,6 +97,12 @@ func TestIntegrationBackendHappyPath(t *testing.T) {
|
||||
rv3, err = writeEvent(ctx, backend, "item3", resource.WatchEvent_ADDED)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, rv3, rv2)
|
||||
|
||||
stats, err := backend.GetResourceStats(ctx, 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stats, 1)
|
||||
require.Equal(t, int64(3), stats[0].Count)
|
||||
require.Equal(t, rv3, stats[0].ResourceVersion)
|
||||
})
|
||||
|
||||
t.Run("Update item2", func(t *testing.T) {
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
SELECT
|
||||
`namespace`,
|
||||
`group`,
|
||||
`resource`,
|
||||
COUNT(*),
|
||||
MAX(`resource_version`)
|
||||
FROM `resource`
|
||||
GROUP BY
|
||||
`namespace`,
|
||||
`group`,
|
||||
`resource`
|
||||
;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
SELECT
|
||||
"namespace",
|
||||
"group",
|
||||
"resource",
|
||||
COUNT(*),
|
||||
MAX("resource_version")
|
||||
FROM "resource"
|
||||
GROUP BY
|
||||
"namespace",
|
||||
"group",
|
||||
"resource"
|
||||
;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
SELECT
|
||||
"namespace",
|
||||
"group",
|
||||
"resource",
|
||||
COUNT(*),
|
||||
MAX("resource_version")
|
||||
FROM "resource"
|
||||
GROUP BY
|
||||
"namespace",
|
||||
"group",
|
||||
"resource"
|
||||
;
|
||||
@@ -0,0 +1,130 @@
|
||||
package shorturl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestShortURL(t *testing.T) {
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: true,
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
username, password := "viewer", "viewer"
|
||||
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleEditor),
|
||||
Password: user.Password(password),
|
||||
Login: username,
|
||||
})
|
||||
|
||||
c := client(grafanaListedAddr, username, password)
|
||||
|
||||
// Create a valid short-urls
|
||||
res, err := c.post("/api/short-urls", bytes.NewReader([]byte(`{"path":"explore"}`)))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
bodyRaw, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
resParsed := struct {
|
||||
UID string `json:"uid"`
|
||||
}{}
|
||||
err = json.Unmarshal(bodyRaw, &resParsed)
|
||||
require.NoError(t, err)
|
||||
|
||||
// If the go-to exists, it should be set in the location and the status should match 302.
|
||||
res, err = c.get(fmt.Sprintf("/goto/%s", resParsed.UID))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
require.Equal(t, "http://localhost:3000/explore", res.Header.Get("Location"))
|
||||
require.Equal(t, http.StatusFound, res.StatusCode)
|
||||
|
||||
// If the go-to does not exist, it should redirect to the home page and return 308.
|
||||
res, err = c.get("/goto/DoesNotExist")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
require.Equal(t, "http://localhost:3000/", res.Header.Get("Location"))
|
||||
require.Equal(t, http.StatusPermanentRedirect, res.StatusCode)
|
||||
}
|
||||
|
||||
func createUser(t *testing.T, db db.DB, cfg *setting.Cfg, cmd user.CreateUserCommand) int64 {
|
||||
t.Helper()
|
||||
|
||||
cfg.AutoAssignOrg = true
|
||||
cfg.AutoAssignOrgId = 1
|
||||
|
||||
quotaService := quotaimpl.ProvideService(db, cfg)
|
||||
orgService, err := orgimpl.ProvideService(db, cfg, quotaService)
|
||||
require.NoError(t, err)
|
||||
usrSvc, err := userimpl.ProvideService(
|
||||
db, orgService, cfg, nil, nil, tracing.InitializeTracerForTest(),
|
||||
quotaService, supportbundlestest.NewFakeBundleService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
u, err := usrSvc.Create(context.Background(), &cmd)
|
||||
require.NoError(t, err)
|
||||
return u.ID
|
||||
}
|
||||
|
||||
type apiClient struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func client(host, user, pass string) apiClient {
|
||||
if len(user) == 0 && len(pass) == 0 {
|
||||
return apiClient{url: fmt.Sprintf("http://%s", host)}
|
||||
}
|
||||
return apiClient{url: fmt.Sprintf("http://%s:%s@%s", user, pass, host)}
|
||||
}
|
||||
|
||||
func (a apiClient) get(path string) (*http.Response, error) {
|
||||
u := fmt.Sprintf("%s%s", a.url, path)
|
||||
// Setup a client that doesn't follow redirects.
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
// nolint:gosec
|
||||
return client.Get(u)
|
||||
}
|
||||
|
||||
func (a apiClient) post(path string, body io.Reader) (*http.Response, error) {
|
||||
u := fmt.Sprintf("%s%s", a.url, path)
|
||||
// nolint:gosec
|
||||
return http.Post(u, "application/json", body)
|
||||
}
|
||||
@@ -59,7 +59,8 @@ func (e *cloudWatchExecutor) executeAnnotationQuery(ctx context.Context, pluginC
|
||||
|
||||
cli, err := e.getCWClient(ctx, pluginCtx, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
result = errorsource.AddDownstreamErrorToResponse(query.RefID, result, fmt.Errorf("%v: %w", "failed to get client", err))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var alarmNames []*string
|
||||
|
||||
@@ -24,7 +24,8 @@ var executeSyncLogQuery = func(ctx context.Context, e *cloudWatchExecutor, req *
|
||||
|
||||
instance, err := e.getInstance(ctx, req.PluginContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorsource.AddErrorToResponse(req.Queries[0].RefID, resp, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
for _, q := range req.Queries {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
|
||||
)
|
||||
|
||||
type Duration struct {
|
||||
@@ -61,13 +62,16 @@ func (duration *Duration) UnmarshalJSON(b []byte) error {
|
||||
case float64:
|
||||
*duration = Duration{time.Duration(value)}
|
||||
case string:
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
dur, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return err
|
||||
return errorsource.DownstreamError(err, false)
|
||||
}
|
||||
*duration = Duration{dur}
|
||||
default:
|
||||
return fmt.Errorf("invalid duration: %#v", unmarshalledJson)
|
||||
return errorsource.DownstreamError(fmt.Errorf("invalid duration: %#v", unmarshalledJson), false)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,12 +2,14 @@ package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -114,6 +116,24 @@ func Test_Settings_LoadCloudWatchSettings(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, time.Minute*30, s.LogsTimeout.Duration)
|
||||
})
|
||||
t.Run("Should set logsTimeout to default duration if it is empty string", func(t *testing.T) {
|
||||
settings := backend.DataSourceInstanceSettings{
|
||||
ID: 33,
|
||||
JSONData: []byte(`{
|
||||
"authType": "arn",
|
||||
"assumeRoleArn": "arn:aws:iam::123456789012:role/grafana",
|
||||
"logsTimeout": ""
|
||||
}`),
|
||||
DecryptedSecureJSONData: map[string]string{
|
||||
"accessKey": "AKIAIOSFODNN7EXAMPLE",
|
||||
"secretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
},
|
||||
}
|
||||
|
||||
s, err := LoadCloudWatchSettings(settingCtx, settings)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, time.Minute*30, s.LogsTimeout.Duration)
|
||||
})
|
||||
t.Run("Should correctly parse logsTimeout duration string", func(t *testing.T) {
|
||||
settings := backend.DataSourceInstanceSettings{
|
||||
ID: 33,
|
||||
@@ -184,6 +204,10 @@ func Test_Settings_LoadCloudWatchSettings(t *testing.T) {
|
||||
|
||||
_, err := LoadCloudWatchSettings(context.Background(), settings)
|
||||
require.Error(t, err)
|
||||
var sourceErr errorsource.Error
|
||||
ok := errors.As(err, &sourceErr)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, sourceErr.ErrorSource().String(), "downstream")
|
||||
})
|
||||
t.Run("Should throw error if logsTimeout is an invalid type", func(t *testing.T) {
|
||||
settings := backend.DataSourceInstanceSettings{
|
||||
|
||||
@@ -35,7 +35,8 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba
|
||||
|
||||
instance, err := e.getInstance(ctx, req.PluginContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorsource.AddErrorToResponse(req.Queries[0].RefID, resp, err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
requestQueries, err := models.ParseMetricDataQueries(req.Queries, startTime, endTime, instance.Settings.Region, e.logger.FromContext(ctx),
|
||||
|
||||
@@ -3,6 +3,7 @@ package elasticsearch
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/errorsource"
|
||||
)
|
||||
|
||||
func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
||||
@@ -51,6 +53,9 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Failed to do healthcheck request", "error", err, "url", esUrl.String())
|
||||
if backend.IsDownstreamHTTPError(err) {
|
||||
err = errorsource.DownstreamError(err, false)
|
||||
}
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusUnknown,
|
||||
Message: "Failed to do healthcheck request",
|
||||
@@ -64,6 +69,14 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
||||
}, nil
|
||||
}
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
errWithSource := errorsource.SourceError(backend.ErrorSourceFromHTTPStatus(response.StatusCode), fmt.Errorf("unexpected status code: %d", response.StatusCode), false)
|
||||
return &backend.CheckHealthResult{
|
||||
Status: backend.HealthStatusError,
|
||||
Message: fmt.Sprintf("Elasticsearch data source is not healthy. Status: %s", response.Status),
|
||||
}, errWithSource
|
||||
}
|
||||
|
||||
logger.Info("Response received from Elasticsearch", "statusCode", response.StatusCode, "status", "ok", "duration", time.Since(start))
|
||||
|
||||
defer func() {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func Test_Healthcheck_OK(t *testing.T) {
|
||||
service := GetMockService(true)
|
||||
service := GetMockService(http.StatusOK, "200 OK")
|
||||
res, _ := service.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{},
|
||||
Headers: nil,
|
||||
@@ -26,7 +26,7 @@ func Test_Healthcheck_OK(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_Healthcheck_Timeout(t *testing.T) {
|
||||
service := GetMockService(false)
|
||||
service := GetMockService(http.StatusRequestTimeout, "408 Request Timeout")
|
||||
res, _ := service.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{},
|
||||
Headers: nil,
|
||||
@@ -35,13 +35,24 @@ func Test_Healthcheck_Timeout(t *testing.T) {
|
||||
assert.Equal(t, "Elasticsearch data source is not healthy", res.Message)
|
||||
}
|
||||
|
||||
func Test_Healthcheck_Error(t *testing.T) {
|
||||
service := GetMockService(http.StatusBadGateway, "502 Bad Gateway")
|
||||
res, _ := service.CheckHealth(context.Background(), &backend.CheckHealthRequest{
|
||||
PluginContext: backend.PluginContext{},
|
||||
Headers: nil,
|
||||
})
|
||||
assert.Equal(t, backend.HealthStatusError, res.Status)
|
||||
assert.Equal(t, "Elasticsearch data source is not healthy. Status: 502 Bad Gateway", res.Message)
|
||||
}
|
||||
|
||||
type FakeRoundTripper struct {
|
||||
isDsHealthy bool
|
||||
statusCode int
|
||||
status string
|
||||
}
|
||||
|
||||
func (fakeRoundTripper *FakeRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
var res *http.Response
|
||||
if fakeRoundTripper.isDsHealthy {
|
||||
if fakeRoundTripper.statusCode == http.StatusOK {
|
||||
res = &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Status: "200 OK",
|
||||
@@ -49,8 +60,8 @@ func (fakeRoundTripper *FakeRoundTripper) RoundTrip(req *http.Request) (*http.Re
|
||||
}
|
||||
} else {
|
||||
res = &http.Response{
|
||||
StatusCode: http.StatusRequestTimeout,
|
||||
Status: "408 Request Timeout",
|
||||
StatusCode: fakeRoundTripper.statusCode,
|
||||
Status: fakeRoundTripper.status,
|
||||
Body: io.NopCloser(bytes.NewBufferString("{\"status\":\"red\"}")),
|
||||
}
|
||||
}
|
||||
@@ -58,12 +69,13 @@ func (fakeRoundTripper *FakeRoundTripper) RoundTrip(req *http.Request) (*http.Re
|
||||
}
|
||||
|
||||
type FakeInstanceManager struct {
|
||||
isDsHealthy bool
|
||||
statusCode int
|
||||
status string
|
||||
}
|
||||
|
||||
func (fakeInstanceManager *FakeInstanceManager) Get(tx context.Context, pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
|
||||
httpClient, _ := httpclient.New(httpclient.Options{})
|
||||
httpClient.Transport = &FakeRoundTripper{isDsHealthy: fakeInstanceManager.isDsHealthy}
|
||||
httpClient.Transport = &FakeRoundTripper{statusCode: fakeInstanceManager.statusCode, status: fakeInstanceManager.status}
|
||||
|
||||
return es.DatasourceInfo{
|
||||
HTTPClient: httpClient,
|
||||
@@ -74,9 +86,9 @@ func (*FakeInstanceManager) Do(_ context.Context, _ backend.PluginContext, _ ins
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetMockService(isDsHealthy bool) *Service {
|
||||
func GetMockService(statusCode int, status string) *Service {
|
||||
return &Service{
|
||||
im: &FakeInstanceManager{isDsHealthy: isDsHealthy},
|
||||
im: &FakeInstanceManager{statusCode: statusCode, status: status},
|
||||
logger: log.New(),
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
xorm.io/builder v0.3.6
|
||||
xorm.io/core v0.7.3
|
||||
)
|
||||
|
||||
@@ -30,8 +30,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
||||
@@ -4501,6 +4501,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmailSchema": {
|
||||
"description": "Implementation of SCIM specification email schema\nhttps://datatracker.ietf.org/doc/html/rfc7643#section-4.1.2",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EnumFieldConfig": {
|
||||
"description": "Enum field config\nVector values are used as lookup keys into the enum fields",
|
||||
"type": "object",
|
||||
@@ -5016,6 +5031,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetUsersBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/UserSchema"
|
||||
}
|
||||
},
|
||||
"itemsPerPage": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"schemas": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"startIndex": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"totalResults": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Group": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -5553,6 +5597,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"MetaSchema": {
|
||||
"description": "Implementation of SCIM specification meta schema\nhttps://datatracker.ietf.org/doc/html/rfc7643#section-3.1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"lastModified": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"resourceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Metadata": {
|
||||
"description": "Metadata contains user accesses for a given resource\nEx: map[string]bool{\"create\":true, \"delete\": true}",
|
||||
"type": "object",
|
||||
@@ -5737,6 +5804,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"NameSchema": {
|
||||
"description": "Implementation of SCIM specification name schema\nhttps://datatracker.ietf.org/doc/html/rfc7643#section-4.1.1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"familyName": {
|
||||
"type": "string"
|
||||
},
|
||||
"formatted": {
|
||||
"type": "string"
|
||||
},
|
||||
"givenName": {
|
||||
"type": "string"
|
||||
},
|
||||
"middleName": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NavbarPreference": {
|
||||
"type": "object",
|
||||
"title": "NavbarPreference defines model for NavbarPreference.",
|
||||
@@ -8566,6 +8651,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserSchema": {
|
||||
"description": "Implementation of SCIM specification user schema\nhttps://datatracker.ietf.org/doc/html/rfc7643#section-4.1.1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"emails": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/EmailSchema"
|
||||
}
|
||||
},
|
||||
"externalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/MetaSchema"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/definitions/NameSchema"
|
||||
},
|
||||
"schemas": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"userName": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserSearchHitDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9769,6 +9893,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"getUsers": {
|
||||
"description": "",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/GetUsersBody"
|
||||
}
|
||||
},
|
||||
"goneError": {
|
||||
"description": "GoneError is returned when the requested endpoint was removed.",
|
||||
"schema": {
|
||||
|
||||
@@ -8607,6 +8607,30 @@
|
||||
}
|
||||
},
|
||||
"/scim/users/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"summary": "Fetches all users in UserSchema format.",
|
||||
"operationId": "getUsers",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/getUsers"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/badRequestError"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/responses/unauthorisedError"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFoundError"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/responses/internalServerError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"users"
|
||||
@@ -8617,6 +8641,9 @@
|
||||
"200": {
|
||||
"$ref": "#/responses/okResponse"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/badRequestError"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/responses/unauthorisedError"
|
||||
},
|
||||
@@ -8630,6 +8657,56 @@
|
||||
"$ref": "#/responses/internalServerError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Deletes user.",
|
||||
"operationId": "deleteUser",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/okResponse"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/badRequestError"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/responses/unauthorisedError"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFoundError"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/responses/internalServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/scim/users/:id": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Gets user by id.",
|
||||
"operationId": "getUser",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/okResponse"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/responses/badRequestError"
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/responses/unauthorisedError"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbiddenError"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/responses/internalServerError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/search": {
|
||||
@@ -15065,6 +15142,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmailSchema": {
|
||||
"description": "Implementation of SCIM specification email schema\nhttps://datatracker.ietf.org/doc/html/rfc7643#section-4.1.2",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"primary": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EmbeddedContactPoint": {
|
||||
"description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.",
|
||||
"type": "object",
|
||||
@@ -15779,6 +15871,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"GetUsersBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/UserSchema"
|
||||
}
|
||||
},
|
||||
"itemsPerPage": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"schemas": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"startIndex": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"totalResults": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"GettableAlertmanagers": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -17075,6 +17196,29 @@
|
||||
"$ref": "#/definitions/Matcher"
|
||||
}
|
||||
},
|
||||
"MetaSchema": {
|
||||
"description": "Implementation of SCIM specification meta schema\nhttps://datatracker.ietf.org/doc/html/rfc7643#section-3.1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"lastModified": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"resourceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Metadata": {
|
||||
"description": "Metadata contains user accesses for a given resource\nEx: map[string]bool{\"create\":true, \"delete\": true}",
|
||||
"type": "object",
|
||||
@@ -17301,6 +17445,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"NameSchema": {
|
||||
"description": "Implementation of SCIM specification name schema\nhttps://datatracker.ietf.org/doc/html/rfc7643#section-4.1.1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"familyName": {
|
||||
"type": "string"
|
||||
},
|
||||
"formatted": {
|
||||
"type": "string"
|
||||
},
|
||||
"givenName": {
|
||||
"type": "string"
|
||||
},
|
||||
"middleName": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NamespaceConfigResponse": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -22163,6 +22325,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserSchema": {
|
||||
"description": "Implementation of SCIM specification user schema\nhttps://datatracker.ietf.org/doc/html/rfc7643#section-4.1.1",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"active": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"emails": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/EmailSchema"
|
||||
}
|
||||
},
|
||||
"externalId": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/MetaSchema"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/definitions/NameSchema"
|
||||
},
|
||||
"schemas": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"userName": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserSearchHitDTO": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -24138,6 +24339,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"getUsers": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/GetUsersBody"
|
||||
}
|
||||
},
|
||||
"goneError": {
|
||||
"description": "GoneError is returned when the requested endpoint was removed.",
|
||||
"schema": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { UseFormSetValue } from 'react-hook-form';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getAppEvents, getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { Validate } from 'react-hook-form';
|
||||
import { UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { Validate, UseFormSetValue } from 'react-hook-form';
|
||||
|
||||
import { IconName, SelectableValue } from '@grafana/data';
|
||||
import { Settings } from 'app/types';
|
||||
|
||||
@@ -263,19 +263,14 @@ export const browseDashboardsAPI = createApi({
|
||||
for (const dashboardUID of selectedDashboards) {
|
||||
const fullDash: DashboardDTO = await getDashboardAPI().getDashboardDTO(dashboardUID);
|
||||
|
||||
const options = {
|
||||
await getDashboardAPI().saveDashboard({
|
||||
dashboard: fullDash.dashboard,
|
||||
folderUid: destinationUID,
|
||||
overwrite: false,
|
||||
message: '',
|
||||
};
|
||||
|
||||
await baseQuery({
|
||||
url: `/dashboards/db`,
|
||||
method: 'POST',
|
||||
data: options,
|
||||
});
|
||||
}
|
||||
|
||||
return { data: undefined };
|
||||
},
|
||||
onQueryStarted: ({ destinationUID, selectedItems }, { queryFulfilled, dispatch }) => {
|
||||
@@ -314,11 +309,8 @@ export const browseDashboardsAPI = createApi({
|
||||
// Delete all the dashboards sequentially
|
||||
// TODO error handling here
|
||||
for (const dashboardUID of selectedDashboards) {
|
||||
const response = await baseQuery({
|
||||
url: `/dashboards/uid/${dashboardUID}`,
|
||||
method: 'DELETE',
|
||||
showSuccessAlert: false,
|
||||
});
|
||||
const response = getDashboardAPI().deleteDashboard(dashboardUID, false);
|
||||
|
||||
// @ts-expect-error
|
||||
const name = response?.data?.title;
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
||||
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
import { createUsagesNetwork, transformUsagesToNetwork } from '../variables/utils';
|
||||
|
||||
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
||||
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
||||
import { VariableEditorForm } from './variables/VariableEditorForm';
|
||||
import { VariableEditorList } from './variables/VariableEditorList';
|
||||
import { VariablesUnknownTable } from './variables/VariablesUnknownTable';
|
||||
import {
|
||||
EditableVariableType,
|
||||
RESERVED_GLOBAL_VARIABLE_NAME_REGEX,
|
||||
@@ -17,6 +22,7 @@ import {
|
||||
getVariableDefault,
|
||||
getVariableScene,
|
||||
} from './variables/utils';
|
||||
|
||||
export interface VariablesEditViewState extends DashboardEditViewState {
|
||||
editIndex?: number | undefined;
|
||||
}
|
||||
@@ -197,6 +203,22 @@ export class VariablesEditView extends SceneObjectBase<VariablesEditViewState> i
|
||||
|
||||
return [false, null];
|
||||
};
|
||||
|
||||
public getSaveModel = () => {
|
||||
return transformSceneToSaveModel(this.getDashboard());
|
||||
};
|
||||
|
||||
public getUsages = () => {
|
||||
const model = this.getSaveModel();
|
||||
const usages = createUsagesNetwork(this.getVariables(), model);
|
||||
return usages;
|
||||
};
|
||||
|
||||
public getUsagesNetwork = () => {
|
||||
const usages = this.getUsages();
|
||||
const usagesNetwork = transformUsagesToNetwork(usages);
|
||||
return usagesNetwork;
|
||||
};
|
||||
}
|
||||
|
||||
function VariableEditorSettingsListView({ model }: SceneComponentProps<VariablesEditView>) {
|
||||
@@ -206,6 +228,9 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps<Variables
|
||||
const { onDelete, onDuplicated, onOrderChanged, onEdit, onTypeChange, onGoBack, onAdd } = model;
|
||||
const { variables } = model.getVariableSet().useState();
|
||||
const { editIndex } = model.useState();
|
||||
const usagesNetwork = useMemo(() => model.getUsagesNetwork(), [model]);
|
||||
const usages = useMemo(() => model.getUsages(), [model]);
|
||||
const saveModel = model.getSaveModel();
|
||||
|
||||
if (editIndex !== undefined && variables[editIndex]) {
|
||||
const variable = variables[editIndex];
|
||||
@@ -230,12 +255,15 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps<Variables
|
||||
<NavToolbarActions dashboard={dashboard} />
|
||||
<VariableEditorList
|
||||
variables={variables}
|
||||
usages={usages}
|
||||
usagesNetwork={usagesNetwork}
|
||||
onDelete={onDelete}
|
||||
onDuplicate={onDuplicated}
|
||||
onChangeOrder={onOrderChanged}
|
||||
onAdd={onAdd}
|
||||
onEdit={onEdit}
|
||||
/>
|
||||
<VariablesUnknownTable variables={variables} dashboard={saveModel} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,14 @@ import { useStyles2, Stack, Button, EmptyState, TextLink } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { VariablesDependenciesButton } from '../../variables/VariablesDependenciesButton';
|
||||
import { UsagesToNetwork, VariableUsageTree } from '../../variables/utils';
|
||||
|
||||
import { VariableEditorListRow } from './VariableEditorListRow';
|
||||
|
||||
export interface Props {
|
||||
variables: Array<SceneVariable<SceneVariableState>>;
|
||||
usages: VariableUsageTree[];
|
||||
usagesNetwork: UsagesToNetwork[];
|
||||
onAdd: () => void;
|
||||
onChangeOrder: (fromIndex: number, toIndex: number) => void;
|
||||
onDuplicate: (identifier: string) => void;
|
||||
@@ -24,6 +27,8 @@ export interface Props {
|
||||
|
||||
export function VariableEditorList({
|
||||
variables,
|
||||
usages,
|
||||
usagesNetwork,
|
||||
onChangeOrder,
|
||||
onDelete,
|
||||
onDuplicate,
|
||||
@@ -71,6 +76,8 @@ export function VariableEditorList({
|
||||
onDelete={onDelete}
|
||||
onDuplicate={onDuplicate}
|
||||
onEdit={onEdit}
|
||||
usageTree={usages}
|
||||
usagesNetwork={usagesNetwork}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -6,13 +6,18 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { SceneVariable } from '@grafana/scenes';
|
||||
import { Button, ConfirmModal, Icon, IconButton, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { Button, ConfirmModal, Icon, IconButton, Tooltip, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { VariableUsagesButton } from '../../variables/VariableUsagesButton';
|
||||
import { UsagesToNetwork, VariableUsageTree, getVariableUsages } from '../../variables/utils';
|
||||
|
||||
import { getDefinition } from './utils';
|
||||
|
||||
export interface VariableEditorListRowProps {
|
||||
index: number;
|
||||
variable: SceneVariable;
|
||||
usageTree: VariableUsageTree[];
|
||||
usagesNetwork: UsagesToNetwork[];
|
||||
onEdit: (identifier: string) => void;
|
||||
onDuplicate: (identifier: string) => void;
|
||||
onDelete: (identifier: string) => void;
|
||||
@@ -21,6 +26,8 @@ export interface VariableEditorListRowProps {
|
||||
export function VariableEditorListRow({
|
||||
index,
|
||||
variable,
|
||||
usageTree,
|
||||
usagesNetwork,
|
||||
onEdit: propsOnEdit,
|
||||
onDuplicate: propsOnDuplicate,
|
||||
onDelete: propsOnDelete,
|
||||
@@ -30,6 +37,8 @@ export function VariableEditorListRow({
|
||||
const definition = getDefinition(variable);
|
||||
const variableState = variable.state;
|
||||
const identifier = variableState.name;
|
||||
const usages = getVariableUsages(identifier, usageTree);
|
||||
const passed = usages > 0 || variableState.type === 'adhoc';
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const handleDeleteVariableModal = (show: boolean) => () => {
|
||||
setShowDeleteModal(show);
|
||||
@@ -79,6 +88,12 @@ export function VariableEditorListRow({
|
||||
|
||||
<td role="gridcell" className={styles.column}>
|
||||
<div className={styles.icons}>
|
||||
<VariableCheckIndicator passed={passed} />
|
||||
<VariableUsagesButton
|
||||
id={variableState.name}
|
||||
isAdhoc={variableState.type === 'adhoc'}
|
||||
usages={usagesNetwork}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
@@ -122,6 +137,35 @@ export function VariableEditorListRow({
|
||||
);
|
||||
}
|
||||
|
||||
interface VariableCheckIndicatorProps {
|
||||
passed: boolean;
|
||||
}
|
||||
|
||||
function VariableCheckIndicator({ passed }: VariableCheckIndicatorProps): ReactElement {
|
||||
const styles = useStyles2(getStyles);
|
||||
if (passed) {
|
||||
return (
|
||||
<Tooltip content="This variable is referenced by other variables or dashboard.">
|
||||
<Icon
|
||||
name="check"
|
||||
className={styles.iconPassed}
|
||||
aria-label="This variable is referenced by other variables or dashboard."
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content="This variable is not referenced by other variables or dashboard.">
|
||||
<Icon
|
||||
name="exclamation-triangle"
|
||||
className={styles.iconFailed}
|
||||
aria-label="This variable is not referenced by any variable or dashboard."
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
dragHandle: css({
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import * as runtime from '@grafana/runtime';
|
||||
import { TestVariable } from '@grafana/scenes';
|
||||
|
||||
import * as utils from '../../variables/utils';
|
||||
import { UsagesToNetwork } from '../../variables/utils';
|
||||
|
||||
import {
|
||||
SLOW_VARIABLES_EXPANSION_THRESHOLD,
|
||||
VariablesUnknownTable,
|
||||
VariablesUnknownTableProps,
|
||||
} from './VariablesUnknownTable';
|
||||
|
||||
async function getTestContext(
|
||||
overrides: Partial<VariablesUnknownTableProps> | undefined = {},
|
||||
usages: UsagesToNetwork[] = []
|
||||
) {
|
||||
jest.clearAllMocks();
|
||||
const reportInteractionSpy = jest.spyOn(runtime, 'reportInteraction').mockImplementation();
|
||||
const getUnknownsNetworkSpy = jest.spyOn(utils, 'getUnknownsNetwork').mockResolvedValue(usages);
|
||||
const defaults: VariablesUnknownTableProps = {
|
||||
variables: [],
|
||||
dashboard: null,
|
||||
};
|
||||
const props = { ...defaults, ...overrides };
|
||||
const { rerender } = render(<VariablesUnknownTable {...props} />);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('heading', { name: /renamed or missing variables/i })).toBeInTheDocument()
|
||||
);
|
||||
|
||||
return { reportInteractionSpy, getUnknownsNetworkSpy, rerender };
|
||||
}
|
||||
|
||||
describe('VariablesUnknownTable', () => {
|
||||
describe('when rendered', () => {
|
||||
it('then it should render the section header', async () => {
|
||||
await getTestContext();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when expanding the section', () => {
|
||||
it('then it should call getUnknownsNetwork', async () => {
|
||||
const { getUnknownsNetworkSpy } = await getTestContext();
|
||||
|
||||
await userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||
await waitFor(() => expect(getUnknownsNetworkSpy).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it('then it should report the interaction', async () => {
|
||||
const { reportInteractionSpy } = await getTestContext();
|
||||
|
||||
await userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||
|
||||
expect(reportInteractionSpy).toHaveBeenCalledTimes(1);
|
||||
expect(reportInteractionSpy).toHaveBeenCalledWith('Unknown variables section expanded');
|
||||
});
|
||||
|
||||
describe('but when expanding it again without changes to variables or dashboard', () => {
|
||||
it('then it should not call getUnknownsNetwork', async () => {
|
||||
const { getUnknownsNetworkSpy } = await getTestContext();
|
||||
|
||||
await userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||
await waitFor(() => expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'));
|
||||
expect(getUnknownsNetworkSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||
await waitFor(() => expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false'));
|
||||
|
||||
await userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||
await waitFor(() => expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true'));
|
||||
|
||||
expect(getUnknownsNetworkSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and there are no renamed or missing variables', () => {
|
||||
it('then it should render the correct message', async () => {
|
||||
await getTestContext();
|
||||
|
||||
await userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||
|
||||
expect(screen.getByText('No renamed or missing variables found.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and there are renamed or missing variables', () => {
|
||||
it('then it should render the table', async () => {
|
||||
const variable = new TestVariable({ name: 'Renamed Variable', query: 'A.*', value: '', text: '', options: [] });
|
||||
const usages = [{ variable, nodes: [], edges: [], showGraph: false }];
|
||||
const { reportInteractionSpy } = await getTestContext({}, usages);
|
||||
|
||||
await userEvent.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||
|
||||
expect(screen.queryByText('No renamed or missing variables found.')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Renamed Variable')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Show usages')).toBeInTheDocument();
|
||||
|
||||
// make sure we don't report the interaction for slow expansion
|
||||
expect(reportInteractionSpy).toHaveBeenCalledTimes(1);
|
||||
expect(reportInteractionSpy).toHaveBeenCalledWith('Unknown variables section expanded');
|
||||
});
|
||||
|
||||
describe('but when the unknown processing takes a while', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
// Need to use delay: null here to work with fakeTimers
|
||||
// see https://github.com/testing-library/user-event/issues/833
|
||||
user = userEvent.setup({ delay: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('then it should report slow expansion', async () => {
|
||||
const variable = new TestVariable({
|
||||
name: 'Renamed Variable',
|
||||
query: 'A.*',
|
||||
value: '',
|
||||
text: '',
|
||||
options: [],
|
||||
});
|
||||
const usages = [{ variable, nodes: [], edges: [], showGraph: false }];
|
||||
const { getUnknownsNetworkSpy, reportInteractionSpy } = await getTestContext({}, usages);
|
||||
getUnknownsNetworkSpy.mockImplementation(() => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(usages);
|
||||
}, SLOW_VARIABLES_EXPANSION_THRESHOLD);
|
||||
});
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('heading', { name: /renamed or missing variables/i }));
|
||||
|
||||
jest.advanceTimersByTime(SLOW_VARIABLES_EXPANSION_THRESHOLD);
|
||||
|
||||
// make sure we report the interaction for slow expansion
|
||||
await waitFor(() =>
|
||||
expect(reportInteractionSpy).toHaveBeenCalledWith('Slow unknown variables expansion', {
|
||||
elapsed: expect.any(Number),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { SceneVariable, SceneVariableState } from '@grafana/scenes';
|
||||
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
|
||||
import { CollapsableSection, Icon, Spinner, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { VariableUsagesButton } from '../../variables/VariableUsagesButton';
|
||||
import { getUnknownsNetwork, UsagesToNetwork } from '../../variables/utils';
|
||||
|
||||
export const SLOW_VARIABLES_EXPANSION_THRESHOLD = 1000;
|
||||
|
||||
export interface VariablesUnknownTableProps {
|
||||
variables: Array<SceneVariable<SceneVariableState>>;
|
||||
dashboard: Dashboard | null;
|
||||
}
|
||||
|
||||
export function VariablesUnknownTable({ variables, dashboard }: VariablesUnknownTableProps): ReactElement {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [changed, setChanged] = useState(0);
|
||||
const style = useStyles2(getStyles);
|
||||
|
||||
useEffect(() => setChanged((prevState) => prevState + 1), [variables, dashboard]);
|
||||
|
||||
const [{ loading, value: usages }, getUnknowns] = useAsyncFn(async () => {
|
||||
const start = Date.now();
|
||||
const unknownsNetwork = await getUnknownsNetwork(variables, dashboard);
|
||||
const stop = Date.now();
|
||||
const elapsed = stop - start;
|
||||
if (elapsed >= SLOW_VARIABLES_EXPANSION_THRESHOLD) {
|
||||
reportInteraction('Slow unknown variables expansion', { elapsed });
|
||||
}
|
||||
setChanged(0);
|
||||
|
||||
return unknownsNetwork;
|
||||
}, [variables, dashboard]);
|
||||
|
||||
const onToggle = (isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
reportInteraction('Unknown variables section expanded');
|
||||
|
||||
// make sure we only fetch when opened and variables or dashboard have changed
|
||||
if (changed > 0) {
|
||||
getUnknowns();
|
||||
}
|
||||
}
|
||||
|
||||
setOpen(isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<CollapsableSection label={<CollapseLabel />} isOpen={open} onToggle={onToggle}>
|
||||
{loading || !usages ? (
|
||||
<Stack justifyContent="center" direction="column">
|
||||
<Stack justifyContent="center">
|
||||
<span>
|
||||
<Trans i18nKey="variables.unknown-table.loading">Loading...</Trans>
|
||||
</span>
|
||||
<Spinner />
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : usages.length > 0 ? (
|
||||
<UnknownTable usages={usages} />
|
||||
) : (
|
||||
<NoUnknowns />
|
||||
)}
|
||||
</CollapsableSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CollapseLabel(): ReactElement {
|
||||
const style = useStyles2(getStyles);
|
||||
return (
|
||||
<h5>
|
||||
<Trans i18nKey="variables.unknown-table.renamed-or-missing-variables">Renamed or missing variables</Trans>
|
||||
<Tooltip content="Click to expand a list with all variable references that have been renamed or are missing from the dashboard.">
|
||||
<Icon name="info-circle" className={style.infoIcon} />
|
||||
</Tooltip>
|
||||
</h5>
|
||||
);
|
||||
}
|
||||
|
||||
function NoUnknowns(): ReactElement {
|
||||
return (
|
||||
<span>
|
||||
<Trans i18nKey="variables.unknown-table.no-unknowns">No renamed or missing variables found.</Trans>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function UnknownTable({ usages }: { usages: UsagesToNetwork[] }): ReactElement {
|
||||
const style = useStyles2(getStyles);
|
||||
return (
|
||||
<table className="filter-table filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="variables.unknown-table.variable">Variable</Trans>
|
||||
</th>
|
||||
<th colSpan={5} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{usages.map((usage) => {
|
||||
const name = typeof usage.variable === 'string' ? usage.variable : usage.variable.state.name;
|
||||
return (
|
||||
<tr key={name}>
|
||||
<td className={style.firstColumn}>
|
||||
<span>{name}</span>
|
||||
</td>
|
||||
<td className={style.defaultColumn} />
|
||||
<td className={style.defaultColumn} />
|
||||
<td className={style.defaultColumn} />
|
||||
<td className={style.lastColumn}>
|
||||
<VariableUsagesButton id={name} usages={usages} isAdhoc={false} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
marginTop: theme.spacing(4),
|
||||
paddingTop: theme.spacing(4),
|
||||
}),
|
||||
infoIcon: css({
|
||||
marginLeft: theme.spacing(1),
|
||||
}),
|
||||
defaultColumn: css({
|
||||
width: '1%',
|
||||
}),
|
||||
firstColumn: css({
|
||||
width: '1%',
|
||||
verticalAlign: 'top',
|
||||
color: theme.colors.text.maxContrast,
|
||||
}),
|
||||
lastColumn: css({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
textAlign: 'right',
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { IconButton } from '@grafana/ui';
|
||||
import { NetworkGraphModal } from 'app/features/variables/inspect/NetworkGraphModal';
|
||||
|
||||
import { UsagesToNetwork } from './utils';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
usages: UsagesToNetwork[];
|
||||
isAdhoc: boolean;
|
||||
}
|
||||
|
||||
export const VariableUsagesButton = ({ id, usages, isAdhoc }: Props) => {
|
||||
const network = useMemo(
|
||||
() => usages.find((n) => (typeof n.variable === 'string' ? n.variable : n.variable.state.name) === id),
|
||||
[usages, id]
|
||||
);
|
||||
if (usages.length === 0 || isAdhoc || !network) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodes = network.nodes.map((n) => {
|
||||
if (n.label.includes(`$${id}`)) {
|
||||
return { ...n, color: '#FB7E81' };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
return (
|
||||
<NetworkGraphModal show={false} title={`Showing usages for: $${id}`} nodes={nodes} edges={network.edges}>
|
||||
{({ showModal }) => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
reportInteraction('Show variable usages');
|
||||
showModal();
|
||||
}}
|
||||
name="code-branch"
|
||||
tooltip="Show usages"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</NetworkGraphModal>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,17 @@
|
||||
import { TestVariable } from '@grafana/scenes';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { variableAdapters } from 'app/features/variables/adapters';
|
||||
import { createCustomVariableAdapter } from 'app/features/variables/custom/adapter';
|
||||
import { createDataSourceVariableAdapter } from 'app/features/variables/datasource/adapter';
|
||||
import { createQueryVariableAdapter } from 'app/features/variables/query/adapter';
|
||||
|
||||
import { createDependencyEdges, createDependencyNodes } from './utils';
|
||||
import {
|
||||
createDependencyEdges,
|
||||
getVariableName,
|
||||
createDependencyNodes,
|
||||
createUsagesNetwork,
|
||||
transformUsagesToNetwork,
|
||||
} from './utils';
|
||||
|
||||
variableAdapters.setInit(() => [
|
||||
createDataSourceVariableAdapter(),
|
||||
@@ -12,6 +19,54 @@ variableAdapters.setInit(() => [
|
||||
createQueryVariableAdapter(),
|
||||
]);
|
||||
|
||||
const dashboardMock: Dashboard = {
|
||||
panels: [
|
||||
{
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'gdev-prometheus',
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'gdev-prometheus',
|
||||
},
|
||||
disableTextWrap: false,
|
||||
editorMode: 'code',
|
||||
expr: 'go_gc_duration_seconds{job=$query0)',
|
||||
fullMetaSearch: false,
|
||||
includeNullMetadata: true,
|
||||
instant: false,
|
||||
legendFormat: '__auto',
|
||||
range: true,
|
||||
refId: 'A',
|
||||
useBackend: false,
|
||||
},
|
||||
{
|
||||
datasource: {
|
||||
type: 'prometheus',
|
||||
uid: 'gdev-prometheus',
|
||||
},
|
||||
disableTextWrap: false,
|
||||
editorMode: 'code',
|
||||
expr: 'go_gc_duration_seconds{job=$query1)',
|
||||
fullMetaSearch: false,
|
||||
includeNullMetadata: true,
|
||||
instant: false,
|
||||
legendFormat: '__auto',
|
||||
range: true,
|
||||
refId: 'A',
|
||||
useBackend: false,
|
||||
},
|
||||
],
|
||||
title: 'Panel Title',
|
||||
type: 'timeseries',
|
||||
},
|
||||
],
|
||||
schemaVersion: 40,
|
||||
};
|
||||
|
||||
describe('createDependencyNodes', () => {
|
||||
it('should create node for each variable', () => {
|
||||
const variables = [
|
||||
@@ -38,3 +93,73 @@ describe('createDependencyEdges', () => {
|
||||
expect(graphEdges).toContainEqual({ from: 'C', to: 'B' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUsagesNetwork', () => {
|
||||
it('should create usage network for variables', () => {
|
||||
const variables = [
|
||||
new TestVariable({
|
||||
type: 'query',
|
||||
name: 'query0',
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
new TestVariable({
|
||||
type: 'query',
|
||||
name: 'query1',
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
];
|
||||
|
||||
const usagesNetwork = createUsagesNetwork(variables, dashboardMock);
|
||||
expect(usagesNetwork).toHaveLength(2);
|
||||
expect(usagesNetwork[0].variable.state.name).toBe('query0');
|
||||
expect(usagesNetwork[1].variable.state.name).toBe('query1');
|
||||
});
|
||||
|
||||
it('should not create usage network for variables that are not part of the dashboard', () => {
|
||||
const variables = [
|
||||
new TestVariable({
|
||||
type: 'query',
|
||||
name: 'query3',
|
||||
loading: false,
|
||||
error: null,
|
||||
}),
|
||||
];
|
||||
|
||||
const usagesNetwork = createUsagesNetwork(variables, dashboardMock);
|
||||
expect(usagesNetwork).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformUsagesToNetwork', () => {
|
||||
it('should transform usages to network', () => {
|
||||
const variables = [
|
||||
new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] }),
|
||||
new TestVariable({ name: 'B', query: 'B.*', value: '', text: '', options: [] }),
|
||||
];
|
||||
const usages = [
|
||||
{ variable: variables[0], tree: { key: 'value' } },
|
||||
{ variable: variables[1], tree: { key: 'value' } },
|
||||
];
|
||||
|
||||
const network = transformUsagesToNetwork(usages);
|
||||
expect(network).toHaveLength(2);
|
||||
expect(network[0].nodes).toContainEqual({ id: 'dashboard', label: 'dashboard' });
|
||||
expect(network[0].edges).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVariableName', () => {
|
||||
it('should return undefined if no match is found', () => {
|
||||
expect(getVariableName('no variable here')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if variable matches inherited object prop names', () => {
|
||||
expect(getVariableName('${toString}')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the variable name if it exists and does not match inherited object prop names', () => {
|
||||
expect(getVariableName('${myVariable}')).toBe('myVariable');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { SceneVariable, SceneVariableState } from '@grafana/scenes';
|
||||
import { GraphEdge, GraphNode } from 'app/features/variables/inspect/utils';
|
||||
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
|
||||
import { safeStringifyValue } from 'app/core/utils/explore';
|
||||
import { GraphEdge, GraphNode, getPropsWithVariable } from 'app/features/variables/inspect/utils';
|
||||
|
||||
export const variableRegex = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g;
|
||||
|
||||
export function createDependencyNodes(variables: Array<SceneVariable<SceneVariableState>>): GraphNode[] {
|
||||
return variables.map((variable) => ({ id: variable.state.name, label: `${variable.state.name}` }));
|
||||
@@ -26,3 +30,222 @@ export const createDependencyEdges = (variables: Array<SceneVariable<SceneVariab
|
||||
|
||||
return edges;
|
||||
};
|
||||
|
||||
export interface VariableUsageTree {
|
||||
variable: SceneVariable<SceneVariableState>;
|
||||
tree: unknown;
|
||||
}
|
||||
|
||||
export interface UsagesToNetwork {
|
||||
/** string when unknown/missing variable otherwise SceneVariable */
|
||||
variable: string | SceneVariable<SceneVariableState>;
|
||||
nodes: GraphNode[];
|
||||
edges: GraphEdge[];
|
||||
showGraph: boolean;
|
||||
}
|
||||
|
||||
export function createUsagesNetwork(variables: Array<SceneVariable<SceneVariableState>>, dashboard: Dashboard) {
|
||||
if (!dashboard) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let usages: VariableUsageTree[] = [];
|
||||
|
||||
for (const variable of variables) {
|
||||
const variableId = variable.state.name;
|
||||
const props = getPropsWithVariable(variableId, { key: 'model', value: dashboard }, {});
|
||||
|
||||
if (Object.keys(props).length) {
|
||||
usages.push({ variable, tree: props });
|
||||
}
|
||||
}
|
||||
|
||||
return usages;
|
||||
}
|
||||
|
||||
export function transformUsagesToNetwork(
|
||||
usages: Array<VariableUsageTree | UnknownVariableUsageTree>
|
||||
): UsagesToNetwork[] {
|
||||
const results: UsagesToNetwork[] = [];
|
||||
|
||||
for (const usage of usages) {
|
||||
const { variable, tree } = usage;
|
||||
const result: UsagesToNetwork = {
|
||||
variable,
|
||||
nodes: [{ id: 'dashboard', label: 'dashboard' }],
|
||||
edges: [],
|
||||
showGraph: false,
|
||||
};
|
||||
results.push(traverseTree(result, { id: 'dashboard', value: tree }));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export const traverseTree = (usage: UsagesToNetwork, parent: { id: string; value: unknown }): UsagesToNetwork => {
|
||||
const { id, value } = parent;
|
||||
const { nodes, edges } = usage;
|
||||
|
||||
if (value && typeof value === 'string') {
|
||||
const leafId = `${parent.id}-${value}`;
|
||||
nodes.push({ id: leafId, label: value });
|
||||
edges.push({ from: leafId, to: id });
|
||||
|
||||
return usage;
|
||||
}
|
||||
|
||||
if (isRecord(value)) {
|
||||
const keys = Object.keys(value);
|
||||
for (const key of keys) {
|
||||
const leafId = `${parent.id}-${key}`;
|
||||
nodes.push({ id: leafId, label: key });
|
||||
edges.push({ from: leafId, to: id });
|
||||
usage = traverseTree(usage, { id: leafId, value: value[key] });
|
||||
}
|
||||
|
||||
return usage;
|
||||
}
|
||||
|
||||
return usage;
|
||||
};
|
||||
|
||||
export const getVariableUsages = (variableId: string, usages: VariableUsageTree[]): number => {
|
||||
const usage = usages.find((usage) => usage.variable.state.name === variableId);
|
||||
if (!usage) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isRecord(usage.tree)) {
|
||||
return countLeaves(usage.tree);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const countLeaves = (object: object): number => {
|
||||
const total = Object.values(object).reduce<number>((count, value) => {
|
||||
if (typeof value === 'object') {
|
||||
return count + countLeaves(value);
|
||||
}
|
||||
|
||||
return count + 1;
|
||||
}, 0);
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
export async function getUnknownsNetwork(
|
||||
variables: Array<SceneVariable<SceneVariableState>>,
|
||||
dashboard: Dashboard | null
|
||||
): Promise<UsagesToNetwork[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// can be an expensive call so we avoid blocking the main thread
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const unknowns = createUnknownsNetwork(variables, dashboard);
|
||||
resolve(transformUsagesToNetwork(unknowns));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
type UnknownVariableUsageTree = {
|
||||
variable: string;
|
||||
tree: unknown;
|
||||
};
|
||||
|
||||
function createUnknownsNetwork(
|
||||
variables: Array<SceneVariable<SceneVariableState>>,
|
||||
dashboard: Dashboard | null
|
||||
): UnknownVariableUsageTree[] {
|
||||
if (!dashboard) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let unknown: UnknownVariableUsageTree[] = [];
|
||||
const unknownVariables = getUnknownVariableStrings(variables, dashboard);
|
||||
for (const unknownVariable of unknownVariables) {
|
||||
const props = getPropsWithVariable(unknownVariable, { key: 'model', value: dashboard }, {});
|
||||
if (Object.keys(props).length) {
|
||||
unknown.push({ variable: unknownVariable, tree: props });
|
||||
}
|
||||
}
|
||||
|
||||
return unknown;
|
||||
}
|
||||
|
||||
export const getUnknownVariableStrings = (variables: Array<SceneVariable<SceneVariableState>>, model: Dashboard) => {
|
||||
variableRegex.lastIndex = 0;
|
||||
const unknownVariableNames: string[] = [];
|
||||
const modelAsString = safeStringifyValue(model, 2);
|
||||
const matches = modelAsString.match(variableRegex);
|
||||
|
||||
if (!matches) {
|
||||
return unknownVariableNames;
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match.indexOf('$__') !== -1) {
|
||||
// ignore builtin variables
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match.indexOf('${__') !== -1) {
|
||||
// ignore builtin variables
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match.indexOf('$hashKey') !== -1) {
|
||||
// ignore Angular props
|
||||
continue;
|
||||
}
|
||||
|
||||
const variableName = getVariableName(match);
|
||||
|
||||
if (variables.some((variable) => variable.state.name === variableName)) {
|
||||
// ignore defined variables
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unknownVariableNames.find((name) => name === variableName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variableName) {
|
||||
unknownVariableNames.push(variableName);
|
||||
}
|
||||
}
|
||||
|
||||
return unknownVariableNames;
|
||||
};
|
||||
|
||||
export function getVariableName(expression: string) {
|
||||
const match = variableRegexExec(expression);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const variableName = match.slice(1).find((match) => match !== undefined);
|
||||
|
||||
// ignore variables that match inherited object prop names
|
||||
if (variableName! in {}) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return variableName;
|
||||
}
|
||||
|
||||
// Helper function since lastIndex is not reset
|
||||
export const variableRegexExec = (variableString: string) => {
|
||||
variableRegex.lastIndex = 0;
|
||||
return variableRegex.exec(variableString);
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user