Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
This commit is contained in:
119 files changed
+1135
-714
No files matched your search
+7
-36
@@ -1548,7 +1548,7 @@ exports[`better eslint`] = {
|
||||
"public/app/features/alerting/unified/GrafanaRuleQueryViewer.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/data/src/datetime/rangeutil\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props in text props. Wrap text with <Trans /> or use t() or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
@@ -2470,19 +2470,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rule-viewer/tabs/Routing.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
@@ -5493,12 +5480,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/admin/components/GetStartedWithPlugin/index.ts:5381": [
|
||||
[0, 0, 0, "Do not re-export imported variable (\`./GetStartedWithPlugin\`)", "0"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/InstallControls/ExternallyManagedButton.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
@@ -6332,8 +6313,7 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/NoopMatcherEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
@@ -6341,15 +6321,11 @@ exports[`better eslint`] = {
|
||||
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@@ -6532,10 +6508,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "\'@grafana/data/src/transformations/transformers/histogram\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"]
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"]
|
||||
],
|
||||
"public/app/features/transformers/editors/JoinByFieldTransformerEditor.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/data/src/transformations/transformers/joinByField\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
@@ -6554,9 +6527,7 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/transformers/editors/LimitTransformerEditor.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/data/src/transformations/transformers/limit\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"]
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
|
||||
],
|
||||
"public/app/features/transformers/editors/MergeTransformerEditor.tsx:5381": [
|
||||
[0, 0, 0, "\'@grafana/data/src/transformations/transformers/merge\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
|
||||
|
||||
+5
-5
@@ -179,7 +179,7 @@ steps:
|
||||
name: yarn-install
|
||||
- commands:
|
||||
- apk add --update git bash
|
||||
- yarn betterer ci
|
||||
- yarn betterer:ci
|
||||
depends_on:
|
||||
- yarn-install
|
||||
image: node:22.11.0-alpine
|
||||
@@ -1717,7 +1717,7 @@ steps:
|
||||
name: yarn-install
|
||||
- commands:
|
||||
- apk add --update git bash
|
||||
- yarn betterer ci
|
||||
- yarn betterer:ci
|
||||
depends_on:
|
||||
- yarn-install
|
||||
image: node:22.11.0-alpine
|
||||
@@ -2865,7 +2865,7 @@ steps:
|
||||
name: yarn-install
|
||||
- commands:
|
||||
- apk add --update git bash
|
||||
- yarn betterer ci
|
||||
- yarn betterer:ci
|
||||
depends_on:
|
||||
- yarn-install
|
||||
image: node:22.11.0-alpine
|
||||
@@ -4342,7 +4342,7 @@ steps:
|
||||
name: yarn-install
|
||||
- commands:
|
||||
- apk add --update git bash
|
||||
- yarn betterer ci
|
||||
- yarn betterer:ci
|
||||
depends_on:
|
||||
- yarn-install
|
||||
image: node:22.11.0-alpine
|
||||
@@ -5605,6 +5605,6 @@ kind: secret
|
||||
name: gcr_credentials
|
||||
---
|
||||
kind: signature
|
||||
hmac: a1d8f3defdf52b76049d8a8df1c21dd13cac9a2a9f80566abceffb4acd1b49d2
|
||||
hmac: 7a16cbedacf8a7381304abfde88319ce712a133e24c1ab304543c4c7ac318816
|
||||
|
||||
...
|
||||
@@ -11,7 +11,9 @@ on:
|
||||
type: string
|
||||
required: true
|
||||
secrets:
|
||||
token:
|
||||
GRAFANA_DELIVERY_BOT_APP_ID:
|
||||
required: true
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM:
|
||||
required: true
|
||||
outputs:
|
||||
branch:
|
||||
@@ -26,7 +28,9 @@ on:
|
||||
type: string
|
||||
required: true
|
||||
secrets:
|
||||
token:
|
||||
GRAFANA_DELIVERY_BOT_APP_ID:
|
||||
required: true
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM:
|
||||
required: true
|
||||
jobs:
|
||||
main:
|
||||
@@ -34,10 +38,16 @@ jobs:
|
||||
outputs:
|
||||
branch: ${{ steps.branch.outputs.branch }}
|
||||
steps:
|
||||
- name: "Generate token"
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
|
||||
with:
|
||||
app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
|
||||
private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
- name: Create release branch
|
||||
id: branch
|
||||
uses: grafana/grafana-github-actions-go/bump-release@main
|
||||
with:
|
||||
ownerRepo: ${{ inputs.ownerRepo }}
|
||||
source: ${{ inputs.source }}
|
||||
token: ${{ secrets.token }}
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
@@ -16,7 +16,9 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
token:
|
||||
GRAFANA_DELIVERY_BOT_APP_ID:
|
||||
required: true
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM:
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -33,17 +35,25 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
token:
|
||||
GRAFANA_DELIVERY_BOT_APP_ID:
|
||||
required: true
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Generate token"
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
|
||||
with:
|
||||
app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
|
||||
private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
- name: Migrate PRs
|
||||
uses: grafana/grafana-github-actions-go/migrate-open-prs@main
|
||||
with:
|
||||
token: ${{ secrets.token }}
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
ownerRepo: ${{ inputs.ownerRepo }}
|
||||
from: ${{ inputs.from }}
|
||||
to: ${{ inputs.to }}
|
||||
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
- closed
|
||||
branches:
|
||||
- 'main'
|
||||
- 'v*.*.*'
|
||||
- 'release-*.*.*'
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
@@ -30,15 +30,8 @@ jobs:
|
||||
release_branch: ${{ steps.output.outputs.release_branch }}
|
||||
dry_run: ${{ steps.output.outputs.dry_run }}
|
||||
latest: ${{ steps.output.outputs.latest }}
|
||||
token: ${{ steps.output.outputs.token }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Generate token"
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
|
||||
with:
|
||||
app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
|
||||
private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
# The github-release action expects a `LATEST` value of a string of either '1' or '0'
|
||||
- if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
run: |
|
||||
@@ -58,7 +51,6 @@ jobs:
|
||||
echo "version: $VERSION"
|
||||
|
||||
echo "release_branch=$(echo $VERSION | sed -s 's/^v/release-/g')" >> "$GITHUB_OUTPUT"
|
||||
echo "token=${{ steps.generate_token.outputs.token }}" >> "$GITHUB_OUTPUT"
|
||||
echo "dry_run=$DRY_RUN" >> "$GITHUB_OUTPUT"
|
||||
echo "latest=$LATEST" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
@@ -66,42 +58,46 @@ jobs:
|
||||
name: Create next release branch (Grafana)
|
||||
needs: setup
|
||||
uses: ./.github/workflows/create-next-release-branch.yml
|
||||
secrets:
|
||||
GRAFANA_DELIVERY_BOT_APP_ID: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
with:
|
||||
ownerRepo: 'grafana/grafana'
|
||||
source: ${{ needs.setup.outputs.release_branch }}
|
||||
secrets:
|
||||
token: ${{ needs.setup.outputs.token }}
|
||||
create_next_release_branch_enterprise:
|
||||
name: Create next release branch (Grafana Enterprise)
|
||||
needs: setup
|
||||
uses: ./.github/workflows/create-next-release-branch.yml
|
||||
with:
|
||||
ownerRepo: 'grafana/grafana'
|
||||
source: ${{ needs.setup.outputs.release_branch }}
|
||||
secrets:
|
||||
token: ${{ needs.setup.outputs.token }}
|
||||
GRAFANA_DELIVERY_BOT_APP_ID: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
with:
|
||||
ownerRepo: 'grafana/grafana-enterprise'
|
||||
source: ${{ needs.setup.outputs.release_branch }}
|
||||
migrate_prs_grafana:
|
||||
needs:
|
||||
- setup
|
||||
- create_next_release_branch_grafana
|
||||
uses: ./.github/workflows/migrate-prs.yml
|
||||
secrets:
|
||||
GRAFANA_DELIVERY_BOT_APP_ID: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
with:
|
||||
ownerRepo: 'grafana/grafana'
|
||||
from: ${{ needs.setup.outputs.release_branch }}
|
||||
to: ${{ needs.create_next_release_branch_grafana.outputs.branch }}
|
||||
secrets:
|
||||
token: ${{ needs.setup.outputs.token }}
|
||||
migrate_prs_enterprise:
|
||||
needs:
|
||||
- setup
|
||||
- create_next_release_branch_enterprise
|
||||
uses: ./.github/workflows/migrate-prs.yml
|
||||
secrets:
|
||||
GRAFANA_DELIVERY_BOT_APP_ID: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }}
|
||||
GRAFANA_DELIVERY_BOT_APP_PEM: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }}
|
||||
with:
|
||||
ownerRepo: 'grafana/grafana-enterprise'
|
||||
from: ${{ needs.setup.outputs.release_branch }}
|
||||
to: ${{ needs.create_next_release_branch_enterprise.outputs.branch }}
|
||||
secrets:
|
||||
token: ${{ needs.setup.outputs.token }}
|
||||
post_changelog_on_forum:
|
||||
needs: setup
|
||||
uses: ./.github/workflows/community-release.yml
|
||||
|
||||
@@ -74,6 +74,7 @@ jobs:
|
||||
ref: ${{ steps.branch.outputs.branch }}
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Checkout Grafana (main)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -81,6 +82,7 @@ jobs:
|
||||
fetch-depth: '0'
|
||||
fetch-tags: 'false'
|
||||
path: .grafana-main
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Setup nodejs environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
run:
|
||||
timeout: 10m
|
||||
timeout: 15m
|
||||
concurrency: 10
|
||||
allow-parallel-runners: true
|
||||
linters-settings:
|
||||
|
||||
+111
@@ -1,3 +1,114 @@
|
||||
<!-- 11.5.2 START -->
|
||||
|
||||
# 11.5.2 (2025-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Docker:** Use our own glibc 2.40 binaries [#99918](https://github.com/grafana/grafana/pull/99918), [@DanCech](https://github.com/DanCech)
|
||||
- **TransformationFilter:** Include transformation outputs in transformation filtering options [#99878](https://github.com/grafana/grafana/pull/99878), [@Sergej-Vlasov](https://github.com/Sergej-Vlasov)
|
||||
- **grafana-ui:** Update InlineField error prop type to React.ReactNode [#100373](https://github.com/grafana/grafana/pull/100373), [@Clarity-89](https://github.com/Clarity-89)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Alerting:** Allow specifying uid for new rules added to groups [#100450](https://github.com/grafana/grafana/pull/100450), [@yuri-tceretian](https://github.com/yuri-tceretian)
|
||||
- **Alerting:** Allow specifying uid for new rules added to groups [#100450](https://github.com/grafana/grafana/pull/100450), [@yuri-tceretian](https://github.com/yuri-tceretian)
|
||||
- **Alerting:** Call RLock() before reading sendAlertsTo map [#99880](https://github.com/grafana/grafana/pull/99880), [@santihernandezc](https://github.com/santihernandezc)
|
||||
- **Auth:** Fix redirect with JWT auth URL login [#100355](https://github.com/grafana/grafana/pull/100355), [@mgyongyosi](https://github.com/mgyongyosi)
|
||||
- **AuthN:** Refetch user on "ErrUserAlreadyExists" [#100582](https://github.com/grafana/grafana/pull/100582), [@kalleep](https://github.com/kalleep)
|
||||
- **Azure:** Correctly set application insights resource values [#99599](https://github.com/grafana/grafana/pull/99599), [@aangelisc](https://github.com/aangelisc)
|
||||
- **CodeEditor:** Fix cursor alignment [#99863](https://github.com/grafana/grafana/pull/99863), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **DashboardList:** Throttle the re-renders [#100046](https://github.com/grafana/grafana/pull/100046), [@bfmatei](https://github.com/bfmatei)
|
||||
- **Dashboards:** Bring back scripted dashboards [#100633](https://github.com/grafana/grafana/pull/100633), [@dprokop](https://github.com/dprokop)
|
||||
- **Plugin Metrics:** Eliminate data race in plugin metrics middleware [#100078](https://github.com/grafana/grafana/pull/100078), [@clord](https://github.com/clord)
|
||||
- **RBAC:** Don't check folder access if `annotationPermissionUpdate` FT is enabled [#100117](https://github.com/grafana/grafana/pull/100117), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
|
||||
|
||||
<!-- 11.5.2 END -->
|
||||
<!-- 11.4.2 START -->
|
||||
|
||||
# 11.4.2 (2025-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Docker:** Use our own glibc 2.40 binaries [#99924](https://github.com/grafana/grafana/pull/99924), [@DanCech](https://github.com/DanCech)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Auth:** Fix redirect with JWT auth URL login [#100494](https://github.com/grafana/grafana/pull/100494), [@mgyongyosi](https://github.com/mgyongyosi)
|
||||
- **AuthN:** Refetch user on "ErrUserAlreadyExists" [#100585](https://github.com/grafana/grafana/pull/100585), [@kalleep](https://github.com/kalleep)
|
||||
- **Azure:** Correctly set application insights resource values [#99598](https://github.com/grafana/grafana/pull/99598), [@aangelisc](https://github.com/aangelisc)
|
||||
- **Dashboards:** Bring back scripted dashboards [#100629](https://github.com/grafana/grafana/pull/100629), [@dprokop](https://github.com/dprokop)
|
||||
- **Plugin Metrics:** Eliminate data race in plugin metrics middleware [#100077](https://github.com/grafana/grafana/pull/100077), [@clord](https://github.com/clord)
|
||||
- **RBAC:** Don't check folder access if `annotationPermissionUpdate` FT is enabled [#100116](https://github.com/grafana/grafana/pull/100116), [@IevaVasiljeva](https://github.com/IevaVasiljeva)
|
||||
|
||||
<!-- 11.4.2 END -->
|
||||
<!-- 11.3.4 START -->
|
||||
|
||||
# 11.3.4 (2025-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Docker:** Use our own glibc 2.40 binaries [#99923](https://github.com/grafana/grafana/pull/99923), [@DanCech](https://github.com/DanCech)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Auth:** Fix redirect with JWT auth URL login [#100495](https://github.com/grafana/grafana/pull/100495), [@mgyongyosi](https://github.com/mgyongyosi)
|
||||
- **Azure:** Correctly set application insights resource values [#99597](https://github.com/grafana/grafana/pull/99597), [@aangelisc](https://github.com/aangelisc)
|
||||
- **Dashboards:** Bring back scripted dashboards [#100627](https://github.com/grafana/grafana/pull/100627), [@dprokop](https://github.com/dprokop)
|
||||
- **Plugin Metrics:** Eliminate data race in plugin metrics middleware [#100076](https://github.com/grafana/grafana/pull/100076), [@clord](https://github.com/clord)
|
||||
|
||||
<!-- 11.3.4 END -->
|
||||
<!-- 11.2.7 START -->
|
||||
|
||||
# 11.2.7 (2025-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Docker:** Use our own glibc 2.40 binaries [#99922](https://github.com/grafana/grafana/pull/99922), [@DanCech](https://github.com/DanCech)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Azure:** Correctly set application insights resource values [#99596](https://github.com/grafana/grafana/pull/99596), [@aangelisc](https://github.com/aangelisc)
|
||||
|
||||
<!-- 11.2.7 END -->
|
||||
<!-- 11.1.12 START -->
|
||||
|
||||
# 11.1.12 (2025-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Docker:** Use our own glibc 2.40 binaries [#99917](https://github.com/grafana/grafana/pull/99917), [@DanCech](https://github.com/DanCech)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Azure:** Correctly set application insights resource values [#99595](https://github.com/grafana/grafana/pull/99595), [@aangelisc](https://github.com/aangelisc)
|
||||
|
||||
<!-- 11.1.12 END -->
|
||||
<!-- 11.0.11 START -->
|
||||
|
||||
# 11.0.11 (2025-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Docker:** Use our own glibc 2.40 binaries [#100730](https://github.com/grafana/grafana/pull/100730), [@DanCech](https://github.com/DanCech)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Azure:** Correctly set application insights resource values [#99594](https://github.com/grafana/grafana/pull/99594), [@aangelisc](https://github.com/aangelisc)
|
||||
|
||||
<!-- 11.0.11 END -->
|
||||
<!-- 10.4.16 START -->
|
||||
|
||||
# 10.4.16 (2025-02-18)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Docker:** Use our own glibc 2.40 binaries [#99920](https://github.com/grafana/grafana/pull/99920), [@DanCech](https://github.com/DanCech)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Dashboard:** Fix for overwriting an edited dashboard in the old architecture [#100288](https://github.com/grafana/grafana/pull/100288), [@bfmatei](https://github.com/bfmatei)
|
||||
|
||||
<!-- 10.4.16 END -->
|
||||
<!-- 11.5.1 START -->
|
||||
|
||||
# 11.5.1 (2025-02-03)
|
||||
|
||||
+3
-1
@@ -121,7 +121,9 @@ update: ## Fetch the latest version of this Makefile and the `make-docs` script
|
||||
curl -s -LO https://raw.githubusercontent.com/grafana/writers-toolkit/main/docs/make-docs
|
||||
chmod +x make-docs
|
||||
|
||||
.PHONY: topic/%
|
||||
# ls static/templates/ | sed 's/-template\.md//' | xargs
|
||||
TOPIC_TYPES := concept multiple-tasks reference section task tutorial visualization
|
||||
.PHONY: $(patsubst %,topic/%,$(TOPIC_TYPES))
|
||||
topic/%: ## Create a topic from the Writers' Toolkit template. Specify the topic type as the target, for example, `make topic/task TOPIC_PATH=sources/my-new-topic.md`.
|
||||
topic/%:
|
||||
$(if $(TOPIC_PATH),,$(error "You must set the TOPIC_PATH variable to the path where the $(@F) topic will be created. For example: make $(@) TOPIC_PATH=sources/my-new-topic.md"))
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
# [Semantic versioning](https://semver.org/) is used to help the reader identify the significance of changes.
|
||||
# Changes are relevant to this script and the support docs.mk GNU Make interface.
|
||||
#
|
||||
# ## 8.5.1 (2025-02-18)
|
||||
#
|
||||
# ### Fixed
|
||||
#
|
||||
# - PHONY declaration for topic/<KIND> targets.
|
||||
#
|
||||
# ## 8.5.0 (2025-02-13)
|
||||
#
|
||||
# ### Added
|
||||
|
||||
+76
-110
@@ -14,11 +14,32 @@ labels:
|
||||
menuTitle: Amazon SNS
|
||||
title: Configure Amazon SNS for Alerting
|
||||
weight: 100
|
||||
refs:
|
||||
notification-templates:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/template-notifications/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/
|
||||
configure-contact-points:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/manage-contact-points/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/
|
||||
test-contact-point:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/manage-contact-points/#test-a-contact-point
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/#test-a-contact-point
|
||||
enable-contact-point-notifications:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/alerting/configure-notifications/manage-contact-points/#enable-notifications-for-a-contact-point
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/#enable-notifications-for-a-contact-point
|
||||
---
|
||||
|
||||
# Configure Amazon SNS for Alerting
|
||||
# Configure Amazon SNS notifications
|
||||
|
||||
Use the Grafana Alerting - Amazon SNS integration to send notifications to Amazon SNS when your alerts are firing. You can receive notifications via the various subscriber channels supported by SNS.
|
||||
Use the Amazon SNS integration in a contact point to send alert notifications to a SNS topic. Then, configure the SNS topic to forward notifications to distinct subscriber channels used in your SNS account.
|
||||
|
||||
## Before you begin
|
||||
|
||||
@@ -27,71 +48,51 @@ Before you begin, ensure you have the following:
|
||||
- **AWS SNS Topic**: An SNS topic to send notifications to.
|
||||
- **AWS IAM Identity with necessary access**: An IAM identity (e.g. user, role) with the necessary permissions to publish messages to the SNS topic.
|
||||
|
||||
For an example setup, see [Example Minimal Setup Using Assumed IAM Role]({{< relref "#example-minimal-setup-using-assumed-iam-role" >}}).
|
||||
For a minimal setup, refer to [Example using an Access Key](#example-using-an-access-key).
|
||||
|
||||
## Adding the SNS Contact Point in Grafana
|
||||
## Configure Amazon SNS for a contact point
|
||||
|
||||
With AWS resources configured, proceed to add SNS as a contact point in Grafana.
|
||||
To create a contact point with a SNS integration, complete the following steps.
|
||||
|
||||
- Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**.
|
||||
- Click on **"Add contact point"**.
|
||||
- **Name**: Enter a descriptive name (e.g., `AWS SNS`).
|
||||
- Choose **"AWS SNS"** from the list of contact point types.
|
||||
1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**.
|
||||
1. Click **+ Add contact point**.
|
||||
1. Enter a name for the contact point.
|
||||
1. From the **Integration** list, select **AWS SNS**.
|
||||
1. Set up the required [settings](#sns-settings) for your SNS configuration.
|
||||
1. Click **Save contact point**.
|
||||
|
||||
### 2. Configure SNS Settings
|
||||
For more details on contact points, including how to test them and enable notifications, refer to [Configure contact points](ref:configure-contact-points).
|
||||
|
||||
#### SNS Settings
|
||||
## SNS Settings
|
||||
|
||||
- **The Amazon SNS API URL**: (Optional) The SNS API URL, e.g., `https://sns.us-east-2.amazonaws.com`. If not specified, the SNS API URL from the SNS SDK will be used.
|
||||
- **Signature Version (sigv4)**: Configures AWS's Signature Verification 4 signing process to sign requests.
|
||||
- **Region**: (Optional) The AWS region. If blank, the region from the default credentials chain is used.
|
||||
- **Access Key**: The AWS API access key.
|
||||
- **Secret Key**: The AWS API secret key.
|
||||
- **Access Key** : (Optional) The AWS API access key.
|
||||
- **Secret Key**: (Optional) The AWS API secret key.
|
||||
> Both `Access Key` and `Secret Key` must be provided together or left blank together.
|
||||
>
|
||||
> If left blank, Grafana searches for credentials using the default credentials chain, including environment variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`), the shared credential file, and EC2 instance roles.
|
||||
- **Profile**: (Optional) Named AWS profile used to authenticate.
|
||||
- **Role ARN**: (Optional) The ARN of an AWS IAM role to assume for authentication, serving as an alternative to using AWS API keys.
|
||||
- **SNS topic ARN**: (Optional) If you don't specify this value, you must specify a value for the `Phone number` or `Target ARN`. If you are using a FIFO SNS topic you should set a message group interval longer than 5 minutes to prevent messages with the same group key being deduplicated by the SNS default deduplication window.
|
||||
- **Phone number**: (Optional) Phone number if message is delivered via SMS in E.164 format. If you don't specify this value, you must specify a value for the `SNS topic ARN` or `Target ARN`.
|
||||
- **Target ARN**: (Optional) The mobile platform endpoint ARN if message is delivered via mobile notifications. If you don't specify this value, you must specify a value for the `SNS topic ARN` or `Phone number`.
|
||||
- **Subject**: (Optional) Customize the subject line or use the default template. This field is templateable.
|
||||
- **Message**: (Optional) Customize the message content or use the default template. This field is templateable.
|
||||
- **Subject**: (Optional) Customize the subject. This field supports [notification templates](ref:notification-templates) and, by default, uses the default title template (`default.title`). It **cannot be an empty string**.
|
||||
- **Message**: (Optional) Customize the message. This field supports [notification templates](ref:notification-templates) and, by default, uses the default message template (`default.message`).
|
||||
- **Attributes**: (Optional) Add any SNS message attributes.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Both `Access Key` and `Secret Key` must be provided together or left blank together. If blank it defaults to a chain of credential
|
||||
providers to search for credentials in environment variables, shared credential file, and EC2 Instance Roles.
|
||||
## Example using an Access Key
|
||||
|
||||
Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
|
||||
{{< /admonition >}}
|
||||
This section outlines a minimal setup to configure Amazon SNS with Alerting.
|
||||
|
||||
### 3. Test & Save the Contact Point
|
||||
|
||||
- Click **"Test"** to verify that the SNS configuration is working correctly.
|
||||
- After the test is successful, click **"Save contact point"** to add the SNS contact point.
|
||||
|
||||
### 4. Next steps
|
||||
|
||||
The SNS contact point is ready to receive alert notifications.
|
||||
|
||||
To add this contact point to your alert, complete the following steps.
|
||||
|
||||
1. In Grafana, navigate to **Alerting** > **Alert rules**.
|
||||
1. Edit or create a new alert rule.
|
||||
1. Scroll down to the **Configure labels and notifications** section.
|
||||
1. Under Notifications click **Select contact point**.
|
||||
1. From the drop-down menu, select the previously created contact point.
|
||||
1. **Click Save rule and exit**.
|
||||
|
||||
## Example Minimal Setup Using Assumed IAM Role
|
||||
|
||||
This section outlines a minimal setup to configure SNS with Grafana using an assumed IAM Role.
|
||||
|
||||
### 1. Create an SNS Topic
|
||||
### 1. Create an SNS Topic and Email Subscriber
|
||||
|
||||
1. **Navigate to SNS in AWS Console**:
|
||||
|
||||
- Go to the [Amazon SNS Console](https://console.aws.amazon.com/sns/v3/home).
|
||||
|
||||
2. **Create a new topic** [[AWS Documentation](https://docs.aws.amazon.com/sns/latest/dg/sns-create-topic.html)]:
|
||||
2. **Create a new topic**:
|
||||
|
||||
- On the **Topics** page, choose **"Create topic"**.
|
||||
- Select **"Standard"** as the type.
|
||||
@@ -99,105 +100,70 @@ This section outlines a minimal setup to configure SNS with Grafana using an ass
|
||||
- **Encryption**: Leave disabled for this minimal setup.
|
||||
- Click **"Create topic"**.
|
||||
|
||||
3. (Optional) **Add an email subscriber to help test** [[AWS Documentation](https://docs.aws.amazon.com/sns/latest/dg/sns-email-notifications.html)]:
|
||||
3. (Optional) **Add an email subscriber to help test**:
|
||||
- Within your newly created topic, click on **"Create subscription"**.
|
||||
- **Protocol**: Choose `Email`.
|
||||
- **Endpoint**: Enter your email address to receive test notifications.
|
||||
- Click **"Create subscription"**.
|
||||
- **Confirm Subscription**: Check your email and confirm the subscription by clicking the provided link.
|
||||
|
||||
### 2. Create an IAM Role
|
||||
### 2. Create an IAM Policy, User, and Access Key
|
||||
|
||||
1. **Navigate to IAM in AWS Console**:
|
||||
|
||||
- Go to the [IAM Console](https://console.aws.amazon.com/iam/home).
|
||||
|
||||
2. **Create a new role** [[AWS Documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user.html)]:
|
||||
2. **Create a new policy**:
|
||||
|
||||
- On the **Roles** page, choose **"Create role"**.
|
||||
- **Trusted Entity**: Select **"This account"**.
|
||||
- Click **"Next"** until the end, name it (e.g., `GrafanaSNSRole`), and click **"Create role"**.
|
||||
|
||||
3. **Attach Inline Policy**:
|
||||
|
||||
- After creating the role, select it and navigate to the **"Permissions"** tab.
|
||||
- Click on **"Add permission"** > **"Create inline policy"**.
|
||||
- On the **Policies** page, choose **"Create policy"**.
|
||||
- Switch to the **"JSON"** tab and paste the following policy, replacing `Resource` with your SNS topic ARN:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["sns:Publish", "sns:GetTopicAttributes"],
|
||||
"Resource": "arn:aws:sns:<region>:<account_id>:<topic_name>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": ["sns:Publish", "sns:GetTopicAttributes"],
|
||||
"Resource": "arn:aws:sns:<region>:<account_id>:<topic_name>"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- Click **"Next"**, name it (e.g., `SNSPublishPolicy`), and click **"Create policy"**.
|
||||
|
||||
### 3. Create an IAM Policy
|
||||
|
||||
1. **Create a new policy to allow assuming the above IAM role** [[AWS Documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_create-console.html)]:
|
||||
|
||||
- In the IAM Console, on the **Policies** page, choose **"Create policy"**.
|
||||
- Switch to the **"JSON"** tab and paste the following policy, replacing `Resource` with the ARN of the role you created earlier:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "sts:AssumeRole",
|
||||
"Resource": "arn:aws:iam::<account_id>:role/GrafanaSNSRole"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
2. **Review and Create**:
|
||||
- Click **"Next"**, name it (e.g., `AssumeSNSRolePolicy`), and click **"Create policy"**.
|
||||
|
||||
### 4. Create an IAM User
|
||||
|
||||
1. **Create a new IAM user to assume the above role** [[AWS Documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html)]:
|
||||
3. **Create a new IAM user and assign the policy**
|
||||
|
||||
- In the IAM Console, on the **Users** page, choose **"Create user"**.
|
||||
- Enter a **User name**, e.g., `grafana-sns-user`.
|
||||
- Enter a **User name**, e.g., `alerting-sns-user`.
|
||||
- Click **"Next"**.
|
||||
- Select **"Attach policies directly"**.
|
||||
- Search for the policy you created earlier (`AssumeSNSRolePolicy`) and select it.
|
||||
- In **Set permissions**, select **"Attach policies directly"**.
|
||||
- Search for the policy you created earlier (`SNSPublishPolicy`) and select it.
|
||||
- Click **"Next"** , and click **"Create user"**.
|
||||
|
||||
2. **Generate credentials**:
|
||||
4. **Create an Access Key**:
|
||||
- Within your newly created user, click on **"Create access key"**.
|
||||
- Select an appropriate use-case, e.g., `Application running outside AWS`.
|
||||
- Click **"Next"** , and click **"Create access key"**.
|
||||
- **Save Credentials**: Note the **Access key ID** and **Secret access key**. You'll need these for Grafana's configuration.
|
||||
- **Save Credentials**: Note the **Access key ID** and **Secret access key** that are required in the next step.
|
||||
|
||||
### 5. Add the SNS Contact Point in Grafana
|
||||
### 3. Configure the SNS Contact Point in Grafana
|
||||
|
||||
After creating the IAM user and obtaining the necessary credentials, proceed to [configure the SNS contact point in Grafana]({{< relref "#adding-the-sns-contact-point-in-grafana" >}}) using the following details:
|
||||
Follow the steps in [configure Amazon SNS for a contact point](#configure-amazon-sns-for-a-contact-point), using the settings below and replacing the placeholders with the SNS and IAM values created in the previous steps.
|
||||
|
||||
- **The Amazon SNS API URL**: `https://sns.us-east-1.amazonaws.com`
|
||||
- **The Amazon SNS API URL**: `https://sns.<region>.amazonaws.com`
|
||||
- **Signature Version (sigv4)**:
|
||||
- **Region**: `us-east-1`
|
||||
- **Region**: `<region>`
|
||||
- **Access Key**: `<YOUR_ACCESS_KEY>`.
|
||||
- **Secret Key**: `<YOUR_SECRET_ACCESS_KEY>`
|
||||
- **Role ARN**: `arn:aws:iam::<account_id>:role/GrafanaSNSRole`
|
||||
- **SNS topic ARN**: `arn:aws:sns:<region>:<account_id>:My-Topic`
|
||||
- **SNS topic ARN**: `arn:aws:sns:<region>:<account_id>:<topic_name>`
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Replace the placeholder values (`https://sns.us-east-1.amazonaws.com`, `us-east-1`, `<YOUR_ACCESS_KEY>`, `<YOUR_SECRET_ACCESS_KEY>`, `arn:aws:iam::<account_id>:role/GrafanaSNSRole`, `arn:aws:sns:<region>:<account_id>:My-Topic`) with your actual AWS credentials and ARNs.
|
||||
{{< /admonition >}}
|
||||
[Test the contact point](ref:test-contact-point) to ensure it's working, or [enable notifications](ref:enable-contact-point-notifications) for it.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Configure contact points](ref:configure-contact-points)
|
||||
- [Amazon SNS Documentation](https://docs.aws.amazon.com/sns/index.html)
|
||||
- [AWS IAM Documentation](https://docs.aws.amazon.com/iam/index.html)
|
||||
- [Prometheus Alertmanager SNS Integration](https://prometheus.io/docs/alerting/configuration/#sns_config)
|
||||
- [Cloudwatch AWS Authentication]({{< relref "../../../../datasources/aws-cloudwatch/aws-authentication" >}})
|
||||
- [Amazon IAM Documentation](https://docs.aws.amazon.com/iam/index.html)
|
||||
- [Prometheus Alertmanager SNS Configuration](https://prometheus.io/docs/alerting/configuration/#sns_config)
|
||||
+1
-1
@@ -50,7 +50,7 @@ To create a contact point with webhook integration, complete the following steps
|
||||
1. Enter a name for the contact point.
|
||||
1. From the **Integration** list, select **Webhook**.
|
||||
1. In the **URL** field, copy in your Webhook URL.
|
||||
1. (Optional) Configure [additional settings](#settings).
|
||||
1. (Optional) Configure [additional settings](#webhook-settings).
|
||||
1. Click **Save contact point**.
|
||||
|
||||
For more details on contact points, including how to test them and enable notifications, refer to [Configure contact points](ref:configure-contact-points).
|
||||
|
||||
@@ -76,7 +76,7 @@ Each policy consists of a set of label matchers (0 or more) that specify which a
|
||||
{{< figure src="/media/docs/alerting/notification-routing.png" max-width="750px" caption="Matching alert instances with notification policies" alt="Example of a notification policy tree" >}}
|
||||
|
||||
{{< admonition type="tip" >}}
|
||||
For a practical example of routing with notification policies, refer to our [Getting Started with Alert Instances and Notification Routing tutorial](https://grafana.com/tutorials/alerting-get-started-pt4/).
|
||||
For a practical example of routing with notification policies, refer to our [Getting Started with Alert Instances and Notification Routing tutorial](https://grafana.com/tutorials/alerting-get-started-pt2/).
|
||||
{{< /admonition >}}
|
||||
|
||||
## Routing
|
||||
|
||||
@@ -168,9 +168,8 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) |
|
||||
| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint |
|
||||
| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards |
|
||||
| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally |
|
||||
| `kubernetesRestore` | Allow restoring objects in k8s |
|
||||
| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s |
|
||||
| `kubernetesClientDashboardsFolders` | Route the folder and dashboard service requests to k8s |
|
||||
| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) |
|
||||
| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query |
|
||||
| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service |
|
||||
|
||||
@@ -221,10 +221,14 @@ HTTPS protocol is supported in the image renderer v3.11.0 and later.
|
||||
|
||||
Change the protocol of the server, it can be `http` or `https`. Default is `http`.
|
||||
|
||||
```bash
|
||||
HTTP_PROTOCOL=https
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"service": {
|
||||
"protocol": "http"
|
||||
"protocol": "https"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -233,6 +237,11 @@ Change the protocol of the server, it can be `http` or `https`. Default is `http
|
||||
|
||||
Path to the image renderer certificate and key file used to start an HTTPS server.
|
||||
|
||||
```bash
|
||||
HTTP_CERT_FILE=./path/to/cert
|
||||
HTTP_CERT_KEY=./path/to/key
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"service": {
|
||||
@@ -246,6 +255,10 @@ Path to the image renderer certificate and key file used to start an HTTPS serve
|
||||
|
||||
Minimum TLS version allowed. Accepted values are: `TLSv1.2`, `TLSv1.3`. Default is `TLSv1.2`.
|
||||
|
||||
```bash
|
||||
HTTP_MIN_TLS_VERSION=TLSv1.2
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"service": {
|
||||
|
||||
@@ -39,7 +39,7 @@ require (
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // @grafana/grafana-backend-group
|
||||
github.com/bwmarrin/snowflake v0.3.0 // @grafan/grafana-app-platform-squad
|
||||
github.com/centrifugal/centrifuge v0.33.3 // @grafana/grafana-app-platform-squad
|
||||
github.com/crewjam/saml v0.4.13 // @grafana/identity-access-team
|
||||
github.com/crewjam/saml v0.4.14 // @grafana/identity-access-team
|
||||
github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/grafana-backend-group
|
||||
github.com/dolthub/go-mysql-server v0.19.1-0.20250206012855-c216e59c21a7 // @grafana/grafana-datasources-core-services
|
||||
github.com/dolthub/vitess v0.0.0-20250123002143-3b45b8cacbfa // @grafana/grafana-datasources-core-services
|
||||
@@ -75,8 +75,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-20250207161551-04c87cf39038 // @grafana/alerting-backend
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team
|
||||
github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics
|
||||
|
||||
@@ -1530,10 +1530,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-20250207161551-04c87cf39038 h1:dG/UKAjY/KlKp9fY8aEm+gSQHHRmPm5q+9cea3hRSu8=
|
||||
github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc h1:qx8avmRi3wx+SuAn6UV0pKma1fJ0/4POUYNc+2rxZkg=
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc/go.mod h1:HamgITzYqErFp8qLwYM/JwwE3z6fXft0Z+5n0Dx1rUM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752 h1:pIEPQPua6kCkMib/nOn//WLgtiA4urf2RPBViSF6CcE=
|
||||
github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752/go.mod h1:QRxC8NNAbpGx2vz9/XckJcWXtHU3My57byeudPrf0WU=
|
||||
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
|
||||
|
||||
@@ -606,8 +606,11 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
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/dashboard-linter v0.0.0-20241106223805-1e7999311752 h1:pIEPQPua6kCkMib/nOn//WLgtiA4urf2RPBViSF6CcE=
|
||||
github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752/go.mod h1:QRxC8NNAbpGx2vz9/XckJcWXtHU3My57byeudPrf0WU=
|
||||
github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak=
|
||||
github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90=
|
||||
github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU=
|
||||
@@ -1063,6 +1066,7 @@ go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
@@ -1086,6 +1090,7 @@ golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
@@ -1110,12 +1115,14 @@ gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117/go.mod h1:OimBR/bc1wPO9iV4NC2bpyjy3VnAwZh5EBPQdtaE5oo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250102185135-69823020774d h1:NZBSeFsuFS5YrgHMW/8xfTbzNXMshQPNgq2Yb7xipEs=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250102185135-69823020774d/go.mod h1:s4mHJ3FfG8P6A3O+gZ8TVqB3ufjOl9UG3ANCMMwCHmo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"betterer": "betterer --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",
|
||||
"betterer:ci": "betterer ci --tsconfig ./scripts/cli/tsconfig.json",
|
||||
"plugin:build": "nx run-many -t build --projects='tag:scope:plugin'",
|
||||
"plugin:build:commit": "nx run-many -t build:commit --projects='tag:scope:plugin'",
|
||||
"plugin:build:dev": "nx run-many -t dev --projects='tag:scope:plugin' --maxParallel=100",
|
||||
|
||||
@@ -108,9 +108,8 @@ export interface FeatureToggles {
|
||||
kubernetesPlaylists?: boolean;
|
||||
kubernetesSnapshots?: boolean;
|
||||
kubernetesDashboards?: boolean;
|
||||
kubernetesCliDashboards?: boolean;
|
||||
kubernetesRestore?: boolean;
|
||||
kubernetesFoldersServiceV2?: boolean;
|
||||
kubernetesClientDashboardsFolders?: boolean;
|
||||
datasourceQueryTypes?: boolean;
|
||||
queryService?: boolean;
|
||||
queryServiceRewrite?: boolean;
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"@grafana/ui": "11.6.0-pre",
|
||||
"history": "4.10.1",
|
||||
"lodash": "4.17.21",
|
||||
"react-loading-skeleton": "3.5.0",
|
||||
"react-use": "17.6.0",
|
||||
"rxjs": "7.8.1",
|
||||
"tslib": "2.8.1"
|
||||
|
||||
@@ -192,7 +192,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
||||
}
|
||||
metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetDashboard).Inc()
|
||||
// lookup folder title & url
|
||||
if dash.FolderUID != "" && hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if dash.FolderUID != "" && hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
queryResult, err := hs.folderService.Get(ctx, &folder.GetFolderQuery{
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
UID: &dash.FolderUID,
|
||||
|
||||
@@ -55,6 +55,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
||||
publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/star/startest"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
@@ -833,11 +834,11 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
quotaService := quotatest.New(false, nil)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
if dashboardService == nil {
|
||||
dashboardService, err = service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions,
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions)
|
||||
@@ -845,7 +846,9 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
|
||||
dashboardProvisioningService, err := service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions,
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/star"
|
||||
"github.com/grafana/grafana/pkg/services/star/startest"
|
||||
@@ -463,7 +464,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
fStore := folderimpl.ProvideStore(sc.db)
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
acSvc := acimpl.ProvideOSSService(
|
||||
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
|
||||
features, tracing.InitializeTracerForTest(), sc.db, permreg.ProvidePermissionRegistry(), nil,
|
||||
@@ -474,7 +475,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||
sc.cfg, dashStore, folderStore,
|
||||
features, folderPermissions, ac,
|
||||
folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, dualwrite.ProvideTestService(),
|
||||
folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(b, err)
|
||||
|
||||
@@ -491,7 +492,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
SQLStore: sc.db,
|
||||
Features: features,
|
||||
QuotaService: quotaSrv,
|
||||
SearchService: search.ProvideService(sc.cfg, sc.db, starSvc, dashboardSvc, folderServiceWithFlagOn, features),
|
||||
SearchService: search.ProvideService(sc.cfg, sc.db, starSvc, dashboardSvc, folderServiceWithFlagOn, features, sort.ProvideService()),
|
||||
folderService: folderServiceWithFlagOn,
|
||||
DashboardService: dashboardSvc,
|
||||
}
|
||||
|
||||
@@ -625,7 +625,7 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) {
|
||||
|
||||
featuresArr := []any{featuremgmt.FlagNestedFolders}
|
||||
if tc.unifiedStorageEnabled {
|
||||
featuresArr = append(featuresArr, featuremgmt.FlagKubernetesFoldersServiceV2)
|
||||
featuresArr = append(featuresArr, featuremgmt.FlagKubernetesClientDashboardsFolders)
|
||||
}
|
||||
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
|
||||
@@ -3,8 +3,8 @@ module github.com/grafana/grafana/pkg/apimachinery
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // @grafana/identity-access-team
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 // @grafana/identity-access-team
|
||||
github.com/stretchr/testify v1.10.0
|
||||
k8s.io/apimachinery v0.32.1
|
||||
k8s.io/apiserver v0.32.1
|
||||
|
||||
@@ -32,10 +32,10 @@ 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/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/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc h1:qx8avmRi3wx+SuAn6UV0pKma1fJ0/4POUYNc+2rxZkg=
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc/go.mod h1:HamgITzYqErFp8qLwYM/JwwE3z6fXft0Z+5n0Dx1rUM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
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=
|
||||
|
||||
@@ -6,7 +6,7 @@ toolchain go1.23.6
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31
|
||||
github.com/grafana/grafana-app-sdk/logging v0.30.0
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
|
||||
@@ -79,8 +79,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/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.30.0 h1:K/P/bm7Cp7Di4tqIJ3EQz2+842JozQGRaz62r95ApME=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.30.0/go.mod h1:xy6ZyVXl50Z3DBDLybvBPphbykPhuVNed/VNmen9DQM=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 h1:ItDcDxUjVLPKja+hogpqgW/kj8LxUL2qscelXIsN1Bs=
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified"
|
||||
@@ -63,7 +64,7 @@ func ToUnifiedStorage(c utils.CommandLine, cfg *setting.Cfg, sqlStore db.DB) err
|
||||
migrator := legacy.NewDashboardAccess(
|
||||
legacysql.NewDatabaseProvider(sqlStore),
|
||||
authlib.OrgNamespaceFormatter,
|
||||
nil, provisioning, false,
|
||||
nil, provisioning, false, sort.ProvideService(),
|
||||
)
|
||||
|
||||
yes, err := promptYesNo(fmt.Sprintf("Count legacy resources for namespace: %s?", opts.Namespace))
|
||||
|
||||
@@ -20,7 +20,7 @@ type Installer interface {
|
||||
type PluginSource interface {
|
||||
PluginClass(ctx context.Context) Class
|
||||
PluginURIs(ctx context.Context) []string
|
||||
DefaultSignature(ctx context.Context) (Signature, bool)
|
||||
DefaultSignature(ctx context.Context, pluginID string) (Signature, bool)
|
||||
}
|
||||
|
||||
type FileStore interface {
|
||||
|
||||
@@ -494,7 +494,7 @@ func (s *FakePluginSource) PluginURIs(ctx context.Context) []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (s *FakePluginSource) DefaultSignature(ctx context.Context) (plugins.Signature, bool) {
|
||||
func (s *FakePluginSource) DefaultSignature(ctx context.Context, _ string) (plugins.Signature, bool) {
|
||||
if s.DefaultSignatureFunc != nil {
|
||||
return s.DefaultSignatureFunc(ctx)
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ func (s *Signature) ReadPluginManifestFromFS(ctx context.Context, pfs plugins.FS
|
||||
}
|
||||
|
||||
func (s *Signature) Calculate(ctx context.Context, src plugins.PluginSource, plugin plugins.FoundPlugin) (plugins.Signature, error) {
|
||||
if defaultSignature, exists := src.DefaultSignature(ctx); exists {
|
||||
if defaultSignature, exists := src.DefaultSignature(ctx, plugin.JSONData.ID); exists {
|
||||
return defaultSignature, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ func (s *LocalSource) PluginURIs(_ context.Context) []string {
|
||||
return s.paths
|
||||
}
|
||||
|
||||
func (s *LocalSource) DefaultSignature(_ context.Context) (plugins.Signature, bool) {
|
||||
func (s *LocalSource) DefaultSignature(_ context.Context, _ string) (plugins.Signature, bool) {
|
||||
switch s.class {
|
||||
case plugins.ClassCore:
|
||||
return plugins.Signature{
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestSources_List(t *testing.T) {
|
||||
filepath.Join(testdata, "app", "plugins", "datasource"),
|
||||
filepath.Join(testdata, "app", "plugins", "panel"),
|
||||
})
|
||||
sig, exists := srcs[0].DefaultSignature(ctx)
|
||||
sig, exists := srcs[0].DefaultSignature(ctx, "")
|
||||
require.True(t, exists)
|
||||
require.Equal(t, plugins.SignatureStatusInternal, sig.Status)
|
||||
require.Equal(t, plugins.SignatureType(""), sig.Type)
|
||||
@@ -51,7 +51,7 @@ func TestSources_List(t *testing.T) {
|
||||
require.Equal(t, srcs[1].PluginURIs(ctx), []string{
|
||||
filepath.Join(testdata, "pluginRootWithDist", "datasource"),
|
||||
})
|
||||
sig, exists = srcs[1].DefaultSignature(ctx)
|
||||
sig, exists = srcs[1].DefaultSignature(ctx, "")
|
||||
require.False(t, exists)
|
||||
require.Equal(t, plugins.Signature{}, sig)
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestSources_List(t *testing.T) {
|
||||
require.Equal(t, srcs[2].PluginURIs(ctx), []string{
|
||||
filepath.Join(testdata, "pluginRootWithDist", "dist"),
|
||||
})
|
||||
sig, exists = srcs[2].DefaultSignature(ctx)
|
||||
sig, exists = srcs[2].DefaultSignature(ctx, "")
|
||||
require.False(t, exists)
|
||||
require.Equal(t, plugins.Signature{}, sig)
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestSources_List(t *testing.T) {
|
||||
require.Equal(t, srcs[3].PluginURIs(ctx), []string{
|
||||
filepath.Join(testdata, "pluginRootWithDist", "panel"),
|
||||
})
|
||||
sig, exists = srcs[3].DefaultSignature(ctx)
|
||||
sig, exists = srcs[3].DefaultSignature(ctx, "")
|
||||
require.False(t, exists)
|
||||
require.Equal(t, plugins.Signature{}, sig)
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
@@ -42,9 +43,10 @@ type LegacyMigrator interface {
|
||||
func ProvideLegacyMigrator(
|
||||
sql db.DB, // direct access to tables
|
||||
provisioning provisioning.ProvisioningService, // only needed for dashboard settings
|
||||
sort sort.Service,
|
||||
) LegacyMigrator {
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, false)
|
||||
return NewDashboardAccess(dbp, authlib.OrgNamespaceFormatter, nil, provisioning, false, sort)
|
||||
}
|
||||
|
||||
type BlobStoreInfo struct {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||
@@ -68,8 +69,9 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
|
||||
dashStore dashboards.Store,
|
||||
provisioning provisioning.ProvisioningService,
|
||||
softDelete bool,
|
||||
sorter sort.Service,
|
||||
) DashboardAccess {
|
||||
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore)
|
||||
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||
return &dashboardSqlAccess{
|
||||
sql: sql,
|
||||
namespacer: namespacer,
|
||||
|
||||
@@ -14,9 +14,10 @@ import (
|
||||
folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
unisearch "github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"google.golang.org/grpc"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
)
|
||||
@@ -24,10 +25,19 @@ import (
|
||||
type DashboardSearchClient struct {
|
||||
resource.ResourceIndexClient
|
||||
dashboardStore dashboards.Store
|
||||
sorter sort.Service
|
||||
}
|
||||
|
||||
func NewDashboardSearchClient(dashboardStore dashboards.Store) *DashboardSearchClient {
|
||||
return &DashboardSearchClient{dashboardStore: dashboardStore}
|
||||
func NewDashboardSearchClient(dashboardStore dashboards.Store, sorter sort.Service) *DashboardSearchClient {
|
||||
return &DashboardSearchClient{dashboardStore: dashboardStore, sorter: sorter}
|
||||
}
|
||||
|
||||
var sortByMapping = map[string]string{
|
||||
unisearch.DASHBOARD_VIEWS_LAST_30_DAYS: "viewed-recently-",
|
||||
unisearch.DASHBOARD_VIEWS_TOTAL: "viewed-",
|
||||
unisearch.DASHBOARD_ERRORS_LAST_30_DAYS: "errors-recently-",
|
||||
unisearch.DASHBOARD_ERRORS_TOTAL: "errors-",
|
||||
"title": "alpha-",
|
||||
}
|
||||
|
||||
// nolint:gocyclo
|
||||
@@ -78,16 +88,30 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
query.Type = queryType
|
||||
}
|
||||
|
||||
// technically, there exists the ability to register multiple ways of sorting using the legacy database
|
||||
// see RegisterSortOption in pkg/services/search/sorting.go
|
||||
// however, it doesn't look like we are taking advantage of that. And since by default the legacy
|
||||
// sql will sort by title ascending, we only really need to handle the "alpha-desc" case
|
||||
if req.SortBy != nil {
|
||||
for _, sort := range req.SortBy {
|
||||
if sort.Field == "title" && sort.Desc {
|
||||
query.Sort = search.SortAlphaDesc
|
||||
}
|
||||
sortByField := ""
|
||||
if len(req.SortBy) != 0 {
|
||||
if len(req.SortBy) > 1 {
|
||||
return nil, fmt.Errorf("only one sort field is supported")
|
||||
}
|
||||
sort := req.SortBy[0]
|
||||
sortByField = strings.TrimPrefix(sort.Field, resource.SEARCH_FIELD_PREFIX)
|
||||
sorterName := sortByMapping[sortByField]
|
||||
|
||||
if sort.Desc {
|
||||
sorterName += "desc"
|
||||
} else {
|
||||
sorterName += "asc"
|
||||
}
|
||||
|
||||
if sorter, ok := c.sorter.GetSortOption(sorterName); ok {
|
||||
query.Sort = sorter
|
||||
}
|
||||
}
|
||||
|
||||
// the title search will not return any sortMeta (an int64), like
|
||||
// most sorting will. Without this, the title will be set to sortMeta (0)
|
||||
if sortByField == resource.SEARCH_FIELD_TITLE {
|
||||
sortByField = ""
|
||||
}
|
||||
|
||||
// if searching for tags, get those instead of the dashboards or folders
|
||||
@@ -178,7 +202,6 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
query.ProvisionedRepo, _ = dashboard.GetProvisionedFileNameFromMeta(vals[0])
|
||||
}
|
||||
}
|
||||
|
||||
searchFields := resource.StandardSearchFields()
|
||||
list := &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
@@ -186,6 +209,10 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
Name: sortByField,
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -213,7 +240,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
Key: getResourceKey(&dashboards.DashboardSearchProjection{
|
||||
UID: dashboard.UID,
|
||||
}, req.Options.Key.Namespace),
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), {}},
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), {}, {}},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -235,7 +262,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour
|
||||
|
||||
list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{
|
||||
Key: getResourceKey(dashboard, req.Options.Key.Namespace),
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags},
|
||||
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID), tags, []byte(strconv.FormatInt(dashboard.SortMeta, 10))},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -276,6 +303,7 @@ func formatQueryResult(res []dashboards.DashboardSearchProjection) []*dashboards
|
||||
FolderUID: item.FolderUID,
|
||||
Tags: []string{},
|
||||
IsFolder: item.IsFolder,
|
||||
SortMeta: item.SortMeta,
|
||||
}
|
||||
hitList = append(hitList, hit)
|
||||
hits[key] = hit
|
||||
|
||||
@@ -3,14 +3,18 @@ package legacysearcher
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
unisearch "github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
@@ -18,10 +22,13 @@ import (
|
||||
|
||||
func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
mockStore := dashboards.NewFakeDashboardStore(t)
|
||||
client := NewDashboardSearchClient(mockStore)
|
||||
sortSvc := sort.ProvideService()
|
||||
client := NewDashboardSearchClient(mockStore, sortSvc)
|
||||
ctx := context.Background()
|
||||
user := &user.SignedInUser{OrgID: 2}
|
||||
ctx = identity.WithRequester(ctx, user)
|
||||
emptyTags, err := json.Marshal([]string{})
|
||||
require.NoError(t, err)
|
||||
|
||||
dashboardKey := &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
@@ -29,9 +36,11 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("Should parse results into GRPC", func(t *testing.T) {
|
||||
sorter, _ := sortSvc.GetSortOption("alpha-asc")
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: user, // user from context should be used
|
||||
Type: "dash-db", // should set type based off of key
|
||||
Sort: sorter,
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder1", Term: "term"},
|
||||
{UID: "uid2", Title: "Test Dashboard2", FolderUID: "folder2"},
|
||||
@@ -41,17 +50,19 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
},
|
||||
SortBy: []*resource.ResourceSearchRequest_Sort{
|
||||
{
|
||||
Field: resource.SEARCH_FIELD_TITLE,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
tags, err := json.Marshal([]string{"term"})
|
||||
require.NoError(t, err)
|
||||
|
||||
emptyTags, err := json.Marshal([]string{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, resp)
|
||||
|
||||
searchFields := resource.StandardSearchFields()
|
||||
require.Equal(t, &resource.ResourceSearchResponse{
|
||||
TotalHits: 2,
|
||||
@@ -60,6 +71,10 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
Name: "", // sort by should be empty if title is what we sorted by
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
@@ -72,6 +87,7 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
[]byte("Test Dashboard"),
|
||||
[]byte("folder1"),
|
||||
tags,
|
||||
[]byte(strconv.FormatInt(0, 10)),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,6 +100,127 @@ func TestDashboardSearchClient_Search(t *testing.T) {
|
||||
[]byte("Test Dashboard2"),
|
||||
[]byte("folder2"),
|
||||
emptyTags,
|
||||
[]byte(strconv.FormatInt(0, 10)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Sorting should be properly parsed into legacy sorting options (asc), and results added", func(t *testing.T) {
|
||||
sortOptionAsc := model.SortOption{
|
||||
Name: "viewed-asc", // should add -asc to the sort field and match on that
|
||||
}
|
||||
sortSvc.RegisterSortOption(sortOptionAsc)
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: user,
|
||||
Type: "dash-db",
|
||||
Sort: sortOptionAsc,
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(50)},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
},
|
||||
SortBy: []*resource.ResourceSearchRequest_Sort{
|
||||
{
|
||||
Field: resource.SEARCH_FIELD_PREFIX + unisearch.DASHBOARD_VIEWS_TOTAL, // "fields." prefix should be removed
|
||||
Desc: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
searchFields := resource.StandardSearchFields()
|
||||
require.Equal(t, &resource.ResourceSearchResponse{
|
||||
TotalHits: 1,
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
Name: "views_total",
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
Group: dashboard.GROUP,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Test Dashboard"),
|
||||
[]byte("folder"),
|
||||
emptyTags,
|
||||
[]byte(strconv.FormatInt(50, 10)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, resp)
|
||||
mockStore.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Sorting should be properly parsed into legacy sorting options (desc)", func(t *testing.T) {
|
||||
sortOptionAsc := model.SortOption{
|
||||
Name: "errors-recently-desc", // should add -asc to the sort field and match on that
|
||||
}
|
||||
sortSvc.RegisterSortOption(sortOptionAsc)
|
||||
mockStore.On("FindDashboards", mock.Anything, &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: user,
|
||||
Type: "dash-db",
|
||||
Sort: sortOptionAsc,
|
||||
}).Return([]dashboards.DashboardSearchProjection{
|
||||
{UID: "uid", Title: "Test Dashboard", FolderUID: "folder", SortMeta: int64(2)},
|
||||
}, nil).Once()
|
||||
|
||||
req := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
},
|
||||
SortBy: []*resource.ResourceSearchRequest_Sort{
|
||||
{
|
||||
Field: unisearch.DASHBOARD_ERRORS_LAST_30_DAYS,
|
||||
Desc: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Search(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
searchFields := resource.StandardSearchFields()
|
||||
require.Equal(t, &resource.ResourceSearchResponse{
|
||||
TotalHits: 1,
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
searchFields.Field(resource.SEARCH_FIELD_TITLE),
|
||||
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
|
||||
searchFields.Field(resource.SEARCH_FIELD_TAGS),
|
||||
&resource.ResourceTableColumnDefinition{
|
||||
Name: "errors_last_30_days",
|
||||
Type: resource.ResourceTableColumnDefinition_INT64,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
Group: dashboard.GROUP,
|
||||
Resource: dashboard.DASHBOARD_RESOURCE,
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("Test Dashboard"),
|
||||
[]byte("folder"),
|
||||
emptyTags,
|
||||
[]byte(strconv.FormatInt(2, 10)),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -290,7 +290,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if queryParams.Has("sort") {
|
||||
for _, sort := range queryParams["sort"] {
|
||||
if slices.Contains(search.DashboardFields(), sort) {
|
||||
sort = "fields." + sort
|
||||
sort = resource.SEARCH_FIELD_PREFIX + sort
|
||||
}
|
||||
s := &resource.ResourceSearchRequest_Sort{Field: sort}
|
||||
if strings.HasPrefix(sort, "-") {
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
@@ -70,11 +71,12 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
tracing *tracing.TracingService,
|
||||
unified resource.ResourceClient,
|
||||
dual dualwrite.Service,
|
||||
sorter sort.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore)
|
||||
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||
builder := &DashboardsAPIBuilder{
|
||||
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
|
||||
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
|
||||
@@ -88,7 +90,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
|
||||
legacy: &dashboard.DashboardStorage{
|
||||
Resource: dashboardv0alpha1.DashboardResourceInfo,
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
|
||||
TableConverter: dashboardv0alpha1.DashboardResourceInfo.TableConverter(),
|
||||
Features: features,
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
@@ -65,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
sql db.DB,
|
||||
tracing *tracing.TracingService,
|
||||
unified resource.ResourceClient,
|
||||
sorter sort.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
@@ -81,7 +83,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
|
||||
legacy: &dashboard.DashboardStorage{
|
||||
Resource: dashboardv1alpha1.DashboardResourceInfo,
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
|
||||
TableConverter: dashboardv1alpha1.DashboardResourceInfo.TableConverter(),
|
||||
Features: features,
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/apistore"
|
||||
@@ -65,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
sql db.DB,
|
||||
tracing *tracing.TracingService,
|
||||
unified resource.ResourceClient,
|
||||
sorter sort.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
@@ -82,7 +84,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
||||
|
||||
legacy: &dashboard.DashboardStorage{
|
||||
Resource: dashboardv2alpha1.DashboardResourceInfo,
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete),
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, softDelete, sorter),
|
||||
TableConverter: dashboardv2alpha1.DashboardResourceInfo.TableConverter(),
|
||||
Features: features,
|
||||
},
|
||||
|
||||
@@ -66,7 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg,
|
||||
unified resource.ResourceClient,
|
||||
) *FolderAPIBuilder {
|
||||
if !featuremgmt.AnyEnabled(features,
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
||||
featuremgmt.FlagProvisioning) {
|
||||
return nil // skip registration unless opting into Kubernetes folders or unless we want to customize registration when testing
|
||||
|
||||
@@ -120,6 +120,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database"
|
||||
@@ -236,6 +237,7 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(login.AuthInfoService), new(*authinfoimpl.Service)),
|
||||
authinfoimpl.ProvideStore,
|
||||
datasourceproxy.ProvideService,
|
||||
sort.ProvideService,
|
||||
search.ProvideService,
|
||||
searchV2.ProvideService,
|
||||
searchV2.ProvideSearchHTTPService,
|
||||
|
||||
@@ -110,7 +110,7 @@ func ProvideFolderPermissions(
|
||||
// if the folder is not found, this may be on the create path,
|
||||
// where the write path to legacy will then go through the read
|
||||
// path and try to read from both legacy & unified before it exists on both
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) && errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) && errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||
_, err = folderService.GetLegacy(ctx, &folder.GetFolderQuery{
|
||||
UID: &resourceID,
|
||||
OrgID: orgID,
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
@@ -48,7 +49,7 @@ func ProvideFolderPermissions(
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||
fService := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
acSvc := acimpl.ProvideOSSService(
|
||||
cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(),
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -51,9 +52,9 @@ func TestIntegrationAuthorize(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService())
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
alertingStore "github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -63,9 +64,9 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService())
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
|
||||
repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest(), ruleStore, dashSvc)
|
||||
@@ -246,9 +247,9 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService())
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
|
||||
cfg.AnnotationMaximumTagsLength = 60
|
||||
|
||||
@@ -57,7 +57,8 @@ import (
|
||||
// making sure we only register metrics once into legacy registry
|
||||
var registerIntoLegacyRegistryOnce sync.Once
|
||||
|
||||
func readCABundlePEM(path string, devMode bool) ([]byte, error) {
|
||||
//nolint:unused
|
||||
func _readCABundlePEM(path string, devMode bool) ([]byte, error) {
|
||||
if devMode {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -128,8 +129,6 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi
|
||||
},
|
||||
ExtraConfig: aggregatorapiserver.ExtraConfig{
|
||||
DisableRemoteAvailableConditionController: true,
|
||||
ProxyClientCertFile: commandOptions.KubeAggregatorOptions.ProxyClientCertFile,
|
||||
ProxyClientKeyFile: commandOptions.KubeAggregatorOptions.ProxyClientKeyFile,
|
||||
// NOTE: while ProxyTransport can be skipped in the configuration, it allows honoring
|
||||
// DISABLE_HTTP2, HTTPS_PROXY and NO_PROXY env vars as needed
|
||||
ProxyTransport: createProxyTransport(),
|
||||
@@ -137,6 +136,13 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi
|
||||
},
|
||||
}
|
||||
|
||||
if commandOptions.KubeAggregatorOptions.LegacyClientCertAuth {
|
||||
// NOTE: the availability controller below is a bit different and uses the cert/key pair regardless
|
||||
// of the legacy bool, this is because we are still using that for discovery requests
|
||||
aggregatorConfig.ExtraConfig.ProxyClientCertFile = commandOptions.KubeAggregatorOptions.ProxyClientCertFile
|
||||
aggregatorConfig.ExtraConfig.ProxyClientKeyFile = commandOptions.KubeAggregatorOptions.ProxyClientKeyFile
|
||||
}
|
||||
|
||||
if err := commandOptions.KubeAggregatorOptions.ApplyTo(aggregatorConfig, commandOptions.RecommendedOptions.Etcd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -152,10 +158,6 @@ func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig generi
|
||||
return NewConfig(aggregatorConfig, sharedInformerFactory, []builder.APIGroupBuilder{serviceAPIBuilder}, nil), nil
|
||||
}
|
||||
|
||||
_, err = readCABundlePEM(commandOptions.KubeAggregatorOptions.APIServiceCABundleFile, commandOptions.ExtraOptions.DevMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteServices, err := ReadRemoteServices(commandOptions.KubeAggregatorOptions.RemoteServicesFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@@ -51,8 +52,8 @@ type k8sHandler struct {
|
||||
}
|
||||
|
||||
func NewK8sHandler(dual dualwrite.Service, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource,
|
||||
restConfig func(context.Context) *rest.Config, dashStore dashboards.Store, userSvc user.Service, resourceClient resource.ResourceClient) K8sHandler {
|
||||
legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore)
|
||||
restConfig func(context.Context) *rest.Config, dashStore dashboards.Store, userSvc user.Service, resourceClient resource.ResourceClient, sorter sort.Service) K8sHandler {
|
||||
legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter)
|
||||
searchClient := resource.NewSearchClient(dual, gvr.GroupResource(), resourceClient, legacySearcher)
|
||||
|
||||
return &k8sHandler{
|
||||
|
||||
@@ -23,6 +23,7 @@ type KubeAggregatorOptions struct {
|
||||
AlternateDNS []string
|
||||
ProxyClientCertFile string
|
||||
ProxyClientKeyFile string
|
||||
LegacyClientCertAuth bool
|
||||
RemoteServicesFile string
|
||||
APIServiceCABundleFile string
|
||||
}
|
||||
@@ -46,6 +47,9 @@ func (o *KubeAggregatorOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
|
||||
fs.StringVar(&o.ProxyClientKeyFile, "proxy-client-key-file", o.ProxyClientKeyFile,
|
||||
"path to proxy client key file")
|
||||
|
||||
fs.BoolVar(&o.LegacyClientCertAuth, "legacy_client_cert_auth", true,
|
||||
"whether to use legacy client cert auth")
|
||||
}
|
||||
|
||||
func (o *KubeAggregatorOptions) Validate() []error {
|
||||
|
||||
@@ -12,7 +12,9 @@ import (
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/grpcserver/interceptors"
|
||||
@@ -75,7 +77,11 @@ func newAuthenticator(auth authn.Authenticator, tracer tracing.Tracer) intercept
|
||||
info, err := auth.Authenticate(ctx, authn.NewGRPCTokenProvider(md))
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return ctx, err
|
||||
if authn.IsUnauthenticatedErr(err) {
|
||||
return nil, status.Error(codes.Unauthenticated, err.Error())
|
||||
}
|
||||
|
||||
return ctx, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
// FIXME: Add attribute with service subject once https://github.com/grafana/authlib/issues/139 is closed.
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -303,7 +304,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore),
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
parentUID := ""
|
||||
for i := 0; ; i++ {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
@@ -929,7 +930,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
user := &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
@@ -1049,7 +1050,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
|
||||
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
user := &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -96,9 +97,9 @@ func ProvideDashboardServiceImpl(
|
||||
ac accesscontrol.AccessControl, folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer,
|
||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service,
|
||||
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
|
||||
resourceClient resource.ResourceClient, dual dualwrite.Service,
|
||||
resourceClient resource.ResourceClient, dual dualwrite.Service, sorter sort.Service,
|
||||
) (*DashboardServiceImpl, error) {
|
||||
k8sHandler := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService, resourceClient)
|
||||
k8sHandler := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService, resourceClient, sorter)
|
||||
|
||||
dashSvc := &DashboardServiceImpl{
|
||||
cfg: cfg,
|
||||
@@ -152,7 +153,7 @@ func (dr *DashboardServiceImpl) getPermissionsService(isFolder bool) accesscontr
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
u := "a.Map{}
|
||||
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
||||
if err != nil {
|
||||
@@ -188,7 +189,7 @@ func (dr *DashboardServiceImpl) Count(ctx context.Context, scopeParams *quota.Sc
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
resp, err := dr.k8sclient.GetStats(ctx, orgID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -226,7 +227,7 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -267,7 +268,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context,
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
// if dashboard id is 0, it is a new dashboard
|
||||
if dashboardID == 0 {
|
||||
return nil, nil
|
||||
@@ -301,7 +302,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx con
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
if dashboardUID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -368,7 +369,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
|
||||
}
|
||||
|
||||
// Validate folder
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{
|
||||
OrgID: dash.OrgID,
|
||||
UID: &dash.FolderUID,
|
||||
@@ -555,7 +556,7 @@ func (dr *DashboardServiceImpl) ValidateDashboardBeforeSave(ctx context.Context,
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
// check each org for orphaned provisioned dashboards
|
||||
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
||||
if err != nil {
|
||||
@@ -673,7 +674,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
|
||||
}
|
||||
|
||||
var dash *dashboards.Dashboard
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
// save the dashboard but then do NOT return
|
||||
// we want to save the provisioning data to the dashboard_provisioning table still
|
||||
// to ensure we can safely rollback to mode2 if needed
|
||||
@@ -748,7 +749,7 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) saveDashboard(ctx context.Context, cmd *dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return dr.saveDashboardThroughK8s(ctx, cmd, cmd.OrgID)
|
||||
}
|
||||
|
||||
@@ -756,7 +757,7 @@ func (dr *DashboardServiceImpl) saveDashboard(ctx context.Context, cmd *dashboar
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: orgID, UID: uid, IncludeDeleted: true})
|
||||
}
|
||||
|
||||
@@ -815,7 +816,7 @@ func (dr *DashboardServiceImpl) SoftDeleteDashboard(ctx context.Context, orgID i
|
||||
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
|
||||
}
|
||||
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
// deletes in unistore are soft deletes, so we can just delete in the same way
|
||||
return dr.deleteDashboardThroughK8s(ctx, &dashboards.DeleteDashboardCommand{OrgID: orgID, UID: dashboardUID}, true)
|
||||
}
|
||||
@@ -836,7 +837,7 @@ func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId
|
||||
|
||||
// DeleteAllDashboards will delete all dashboards within a given org.
|
||||
func (dr *DashboardServiceImpl) DeleteAllDashboards(ctx context.Context, orgId int64) error {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return dr.deleteAllDashboardThroughK8s(ctx, orgId)
|
||||
}
|
||||
|
||||
@@ -859,7 +860,7 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId
|
||||
|
||||
cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID}
|
||||
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
err := dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -924,7 +925,7 @@ func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashbo
|
||||
// UnprovisionDashboard removes info about dashboard being provisioned. Used after provisioning configs are changed
|
||||
// and provisioned dashboards are left behind but not deleted.
|
||||
func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashboardId int64) error {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -957,7 +958,7 @@ func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashbo
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||
OrgId: query.OrgID,
|
||||
ProvisionedRepo: dashboard.PluginIDRepoName,
|
||||
@@ -1055,7 +1056,7 @@ func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context,
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return dr.getDashboardThroughK8s(ctx, query)
|
||||
}
|
||||
|
||||
@@ -1063,7 +1064,7 @@ func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboa
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1089,7 +1090,7 @@ func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
if query.OrgID == 0 {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
@@ -1237,7 +1238,7 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb
|
||||
}(time.Now())
|
||||
}
|
||||
|
||||
if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
if query.OrgId == 0 {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
@@ -1319,7 +1320,7 @@ func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *das
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetAllDashboards(ctx context.Context) ([]*dashboards.Dashboard, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1331,7 +1332,7 @@ func (dr *DashboardServiceImpl) GetAllDashboards(ctx context.Context) ([]*dashbo
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return dr.listDashboardsThroughK8s(ctx, orgID)
|
||||
}
|
||||
|
||||
@@ -1405,7 +1406,7 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) {
|
||||
if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
res, err := dr.k8sclient.Search(ctx, query.OrgID, &resource.ResourceSearchRequest{
|
||||
Facet: map[string]*resource.ResourceSearchRequest_Facet{
|
||||
"tags": {
|
||||
@@ -1437,7 +1438,7 @@ func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *das
|
||||
}
|
||||
|
||||
func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||
OrgId: orgID,
|
||||
FolderUIDs: folderUIDs,
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -898,7 +899,9 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService())
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
dashboardPermissions := accesscontrolmock.NewMockedPermissionsService()
|
||||
dashboardService, err := ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore,
|
||||
@@ -915,6 +918,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
require.NoError(t, err)
|
||||
@@ -988,7 +992,9 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService())
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
dashboardPermissions := accesscontrolmock.NewMockedPermissionsService()
|
||||
dashboardPermissions.On("SetPermissions",
|
||||
mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
|
||||
@@ -1007,6 +1013,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
service.RegisterDashboardPermissions(dashboardPermissions)
|
||||
@@ -1041,7 +1048,9 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService())
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
service, err := ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore,
|
||||
featuremgmt.WithFeatures(),
|
||||
@@ -1057,6 +1066,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
service.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
|
||||
@@ -1110,7 +1120,9 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService())
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
service, err := ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore,
|
||||
features,
|
||||
@@ -1126,6 +1138,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
service.RegisterDashboardPermissions(dashboardPermissions)
|
||||
@@ -1185,7 +1198,9 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
|
||||
nil,
|
||||
tracer,
|
||||
nil,
|
||||
dualwrite.ProvideTestService())
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
|
||||
service, err := ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore,
|
||||
@@ -1202,6 +1217,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
service.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
|
||||
|
||||
@@ -265,7 +265,7 @@ func TestDashboardService(t *testing.T) {
|
||||
func setupK8sDashboardTests(service *DashboardServiceImpl) (context.Context, *client.MockK8sHandler) {
|
||||
mockCli := new(client.MockK8sHandler)
|
||||
service.k8sclient = mockCli
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders)
|
||||
|
||||
ctx := context.Background()
|
||||
userCtx := &user.SignedInUser{UserID: 1, OrgID: 1}
|
||||
@@ -1096,7 +1096,13 @@ func TestSaveProvisionedDashboard(t *testing.T) {
|
||||
service := &DashboardServiceImpl{
|
||||
cfg: setting.NewCfg(),
|
||||
dashboardStore: &fakeStore,
|
||||
log: log.NewNopLogger(),
|
||||
folderService: &foldertest.FakeService{
|
||||
ExpectedFolder: &folder.Folder{
|
||||
ID: 0,
|
||||
UID: "general",
|
||||
},
|
||||
},
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
origNewDashboardGuardian := guardian.New
|
||||
@@ -1160,6 +1166,9 @@ func TestSaveDashboard(t *testing.T) {
|
||||
service := &DashboardServiceImpl{
|
||||
cfg: setting.NewCfg(),
|
||||
dashboardStore: &fakeStore,
|
||||
folderService: &foldertest.FakeService{
|
||||
ExpectedFolder: &folder.Folder{},
|
||||
},
|
||||
}
|
||||
|
||||
origNewDashboardGuardian := guardian.New
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/database"
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
@@ -119,6 +120,7 @@ func TestValidateDashboardExists(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
s := ProvideService(dsStore, secretsService, dashSvc)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
@@ -42,7 +43,7 @@ type Service struct {
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, features featuremgmt.FeatureToggles,
|
||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, dual dualwrite.Service) dashver.Service {
|
||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, dual dualwrite.Service, sorter sort.Service) dashver.Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
store: &sqlStore{
|
||||
@@ -58,6 +59,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.Dash
|
||||
dashboardStore,
|
||||
userService,
|
||||
unified,
|
||||
sorter,
|
||||
),
|
||||
dashSvc: dashboardService,
|
||||
log: log.New("dashboard-version"),
|
||||
@@ -85,7 +87,7 @@ func (s *Service) Get(ctx context.Context, query *dashver.GetDashboardVersionQue
|
||||
query.DashboardID = id
|
||||
}
|
||||
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
version, err := s.getHistoryThroughK8s(ctx, query.OrgID, query.DashboardUID, query.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -156,7 +158,7 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions
|
||||
query.Limit = 1000
|
||||
}
|
||||
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
versions, err := s.listHistoryThroughK8s(
|
||||
ctx,
|
||||
query.OrgID,
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestDashboardVersionService(t *testing.T) {
|
||||
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
|
||||
mockCli := new(client.MockK8sHandler)
|
||||
dashboardVersionService.k8sclient = mockCli
|
||||
dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
|
||||
dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders)
|
||||
dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil)
|
||||
|
||||
mockCli.On("GetUserFromMeta", mock.Anything, "user:1").Return(&user.User{ID: 1}, nil)
|
||||
@@ -232,7 +232,7 @@ func TestListDashboardVersions(t *testing.T) {
|
||||
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
|
||||
mockCli := new(client.MockK8sHandler)
|
||||
dashboardVersionService.k8sclient = mockCli
|
||||
dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
|
||||
dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders)
|
||||
|
||||
dashboardService.On("GetDashboardUIDByID", mock.Anything,
|
||||
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
|
||||
|
||||
@@ -680,12 +680,6 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesCliDashboards",
|
||||
Description: "Use the k8s client to retrieve dashboards internally",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesRestore",
|
||||
Description: "Allow restoring objects in k8s",
|
||||
@@ -693,10 +687,10 @@ var (
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesFoldersServiceV2",
|
||||
Description: "Use the Folders Service V2, and route Folder Service requests to k8s",
|
||||
Name: "kubernetesClientDashboardsFolders",
|
||||
Description: "Route the folder and dashboard service requests to k8s",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
Owner: grafanaAppPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "datasourceQueryTypes",
|
||||
|
||||
@@ -360,7 +360,6 @@ appPlatformAccessTokens,2024-09-05T16:18:44Z,2024-10-14T10:47:18Z,d5ebaa0ef92ede
|
||||
appSidecar,2024-09-09T12:45:05Z,,5e2ac24890906e5070323d87730dd78a4f885963,Andrej Ocenas
|
||||
vizActions,2024-09-09T14:11:55Z,,af48d3db1eb2d8681843f5997e50fea5e5ea3096,Adela Almasan
|
||||
groupAttributeSync,2024-09-09T15:29:43Z,,6ded6a8872204a818b3795dc733cc5fe5db066a0,Aaron Godin
|
||||
kubernetesFolders,2024-09-10T09:22:08Z,,b12a29a1dac8b9aec4a99be08e1665939cb27dc5,Arati R.
|
||||
alertingFilterV2,2024-09-11T11:29:26Z,,90ee52e8d9c14237f8a57b622c0def7512e657cd,Gilles De Mey
|
||||
improvedExternalSessionHandling,2024-09-17T10:54:39Z,,41cd0f51800d4849345fc0980ca4173967fc8e9e,Misi
|
||||
datasourceAPIServers,2024-09-19T08:28:27Z,,f21a5987a22bcdb596d6a258d2960e4151348b63,Ryan McKinley
|
||||
@@ -407,7 +406,6 @@ feedbackButton,2024-12-02T17:08:15Z,,8a1b89a5ebb847f6b29e92dcea796f00c30431d6,Mi
|
||||
elasticsearchCrossClusterSearch,2024-12-12T22:20:04Z,,b3a12f486eba69e20dd7ff3a3d4dd065ede7a99f,Isabella Siu
|
||||
unifiedHistory,2024-12-13T10:41:18Z,,aac62c89dae1092836a91d1b6ae6bd7127fe676a,Laura Fernández
|
||||
lokiLabelNamesQueryApi,2024-12-13T14:31:41Z,,5ac7443fcec0db412d3333044a82c2c26b5aece7,Sven Grossmann
|
||||
kubernetesCliDashboards,2024-12-13T22:55:43Z,,8f6e9f8ed0a5024a510cc337c9f1e6972bfb23d4,Stephanie Hingtgen
|
||||
useV2DashboardsAPI,2024-12-17T21:17:09Z,,070f0e4457c5967102ef157197073dc2662f6fb8,Dominik Prokop
|
||||
investigationsBackend,2024-12-18T08:31:03Z,,f46c07aba7b6faccd2ecafc83051d1410cacc867,Jackson Coelho
|
||||
unifiedStorageSearchSprinkles,2024-12-18T17:00:54Z,,4837585cab0fd84184a8c6f5d6891f442a2b95f1,owensmallwood
|
||||
@@ -421,9 +419,9 @@ improvedExternalSessionHandlingSAML,2025-01-09T17:02:49Z,,c52ec21c75ab72c2f7d282
|
||||
teamHttpHeadersMimir,2025-01-13T10:42:47Z,,04acbcdef23f673bd6bbfdbbece29c9769ce155a,Eric Leijonmarck
|
||||
ABTestFeatureToggleA,2025-01-13T21:13:13Z,,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs
|
||||
ABTestFeatureToggleB,2025-01-13T21:13:13Z,,009d7f42b3d09b3a6be1f00f07314e2b25af7ebc,Nathan Marrs
|
||||
kubernetesFoldersServiceV2,2025-01-13T21:15:35Z,,766d645d827f5e6e0872ae30e5fe23226ae85785,maicon
|
||||
queryLibraryDashboards,2025-01-14T11:01:15Z,,740cd22fe51a3543c182857f31fc97fd42263306,Ashley Harrison
|
||||
elasticsearchImprovedParsing,2025-01-15T17:05:54Z,,bab55a4cb84f2ba57838f96a492ab9aa7f307957,Adam Yeats
|
||||
grafanaAdvisor,2025-01-20T10:08:00Z,,c1364d6be6f552203ba786f17a89664304b89247,Andres Martinez Gotor
|
||||
datasourceConnectionsTab,2025-01-21T17:39:48Z,,97d8f68b705f9949493079d1833abfe80e7b48f3,Syerikjan Kh
|
||||
unifiedStorageSearchPermissionFiltering,2025-01-22T11:38:37Z,,dd483fc17fa4a2931848e3574cfc31ea6f6530d9,owensmallwood
|
||||
kubernetesClientDashboardsFolders,2025-02-18T21:15:35Z,,766d645d827f5e6e0872ae30e5fe23226ae85785,Stephanie Hingtgen
|
||||
|
@@ -89,9 +89,8 @@ formatString,GA,@grafana/dataviz-squad,false,false,true
|
||||
kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false
|
||||
kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true
|
||||
kubernetesCliDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
kubernetesRestore,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
kubernetesFoldersServiceV2,experimental,@grafana/search-and-storage,false,false,false
|
||||
kubernetesClientDashboardsFolders,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
queryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
|
||||
|
@@ -367,17 +367,13 @@ const (
|
||||
// Use the kubernetes API in the frontend for dashboards
|
||||
FlagKubernetesDashboards = "kubernetesDashboards"
|
||||
|
||||
// FlagKubernetesCliDashboards
|
||||
// Use the k8s client to retrieve dashboards internally
|
||||
FlagKubernetesCliDashboards = "kubernetesCliDashboards"
|
||||
|
||||
// FlagKubernetesRestore
|
||||
// Allow restoring objects in k8s
|
||||
FlagKubernetesRestore = "kubernetesRestore"
|
||||
|
||||
// FlagKubernetesFoldersServiceV2
|
||||
// Use the Folders Service V2, and route Folder Service requests to k8s
|
||||
FlagKubernetesFoldersServiceV2 = "kubernetesFoldersServiceV2"
|
||||
// FlagKubernetesClientDashboardsFolders
|
||||
// Route the folder and dashboard service requests to k8s
|
||||
FlagKubernetesClientDashboardsFolders = "kubernetesClientDashboardsFolders"
|
||||
|
||||
// FlagDatasourceQueryTypes
|
||||
// Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)
|
||||
|
||||
@@ -2227,12 +2227,15 @@
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesCliDashboards",
|
||||
"resourceVersion": "1733520389522",
|
||||
"creationTimestamp": "2024-12-13T22:55:43Z"
|
||||
"name": "kubernetesClientDashboardsFolders",
|
||||
"resourceVersion": "1739913583714",
|
||||
"creationTimestamp": "2025-02-18T21:15:35Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2025-02-18 21:19:43.714638 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use the k8s client to retrieve dashboards internally",
|
||||
"description": "Route the folder and dashboard service requests to k8s",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-app-platform-squad"
|
||||
}
|
||||
@@ -2264,34 +2267,6 @@
|
||||
"hideFromAdminPage": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesFolders",
|
||||
"resourceVersion": "1725863636605",
|
||||
"creationTimestamp": "2024-09-10T09:22:08Z",
|
||||
"deletionTimestamp": "2025-01-22T20:49:15Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2024-09-09 06:33:56.605329 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/search-and-storage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesFoldersServiceV2",
|
||||
"resourceVersion": "1735336477446",
|
||||
"creationTimestamp": "2025-01-13T21:15:35Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use the Folders Service V2, and route Folder Service requests to k8s",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/search-and-storage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesPlaylists",
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
@@ -89,6 +90,7 @@ func ProvideService(
|
||||
tracer tracing.Tracer,
|
||||
resourceClient resource.ResourceClient,
|
||||
dual dualwrite.Service,
|
||||
sorter sort.Service,
|
||||
) *Service {
|
||||
srv := &Service{
|
||||
log: slog.Default().With("logger", "folder-service"),
|
||||
@@ -111,7 +113,7 @@ func ProvideService(
|
||||
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv))
|
||||
ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv))
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
k8sHandler := client.NewK8sHandler(
|
||||
dual,
|
||||
request.GetNamespaceMapper(cfg),
|
||||
@@ -120,6 +122,7 @@ func ProvideService(
|
||||
dashboardStore,
|
||||
userService,
|
||||
resourceClient,
|
||||
sorter,
|
||||
)
|
||||
|
||||
unifiedStore := ProvideUnifiedStore(k8sHandler, userService)
|
||||
@@ -128,7 +131,7 @@ func ProvideService(
|
||||
srv.k8sclient = k8sHandler
|
||||
}
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
dashHandler := client.NewK8sHandler(
|
||||
dual,
|
||||
request.GetNamespaceMapper(cfg),
|
||||
@@ -137,6 +140,7 @@ func ProvideService(
|
||||
dashboardStore,
|
||||
userService,
|
||||
resourceClient,
|
||||
sorter,
|
||||
)
|
||||
srv.dashboardK8sClient = dashHandler
|
||||
}
|
||||
@@ -191,7 +195,7 @@ func (s *Service) DBMigration(db db.DB) {
|
||||
}
|
||||
|
||||
func (s *Service) SearchFolders(ctx context.Context, q folder.SearchFoldersQuery) (model.HitList, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
// TODO:
|
||||
// - implement filtering by alerting folders and k6 folders (see the dashboards store `FindDashboards` method for reference)
|
||||
// - implement fallback on search client in unistore to go to legacy store (will need to read from dashboard store)
|
||||
@@ -202,7 +206,7 @@ func (s *Service) SearchFolders(ctx context.Context, q folder.SearchFoldersQuery
|
||||
}
|
||||
|
||||
func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.getFoldersFromApiServer(ctx, q)
|
||||
}
|
||||
return s.GetFoldersLegacy(ctx, q)
|
||||
@@ -261,7 +265,7 @@ func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.getFromApiServer(ctx, q)
|
||||
}
|
||||
return s.GetLegacy(ctx, q)
|
||||
@@ -402,7 +406,7 @@ func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identi
|
||||
}
|
||||
|
||||
func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.getChildrenFromApiServer(ctx, q)
|
||||
}
|
||||
return s.GetChildrenLegacy(ctx, q)
|
||||
@@ -672,7 +676,7 @@ func (s *Service) deduplicateAvailableFolders(ctx context.Context, folders []*fo
|
||||
}
|
||||
|
||||
func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.getParentsFromApiServer(ctx, q)
|
||||
}
|
||||
return s.GetParentsLegacy(ctx, q)
|
||||
@@ -705,7 +709,7 @@ func (s *Service) getFolderByTitle(ctx context.Context, orgID int64, title strin
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.createOnApiServer(ctx, cmd)
|
||||
}
|
||||
return s.CreateLegacy(ctx, cmd)
|
||||
@@ -824,7 +828,7 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.updateOnApiServer(ctx, cmd)
|
||||
}
|
||||
return s.UpdateLegacy(ctx, cmd)
|
||||
@@ -963,7 +967,7 @@ func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int6
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.deleteFromApiServer(ctx, cmd)
|
||||
}
|
||||
return s.DeleteLegacy(ctx, cmd)
|
||||
@@ -1086,7 +1090,7 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm
|
||||
}
|
||||
|
||||
func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.moveOnApiServer(ctx, cmd)
|
||||
}
|
||||
return s.MoveLegacy(ctx, cmd)
|
||||
@@ -1313,7 +1317,7 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold
|
||||
}
|
||||
|
||||
func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
return s.getDescendantCountsFromApiServer(ctx, q)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
@@ -68,7 +69,7 @@ func TestIntegrationProvideFolderService(t *testing.T) {
|
||||
store := ProvideStore(db)
|
||||
ProvideService(
|
||||
store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
nil, nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2)
|
||||
})
|
||||
@@ -496,8 +497,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
})
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil,
|
||||
client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService())
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -583,7 +583,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff,
|
||||
folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService())
|
||||
folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b)
|
||||
@@ -726,8 +726,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
tc.service.store = nestedFolderStore
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil,
|
||||
client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService())
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -1521,6 +1520,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
@@ -659,71 +658,43 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
|
||||
return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder)
|
||||
}
|
||||
|
||||
// if dashboard restore is on we don't delete public dashboards, the hard delete will take care of it later
|
||||
if !s.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) {
|
||||
// We need a list of dashboard uids inside the folder to delete related dashboards & public dashboards
|
||||
var dashboardUIDs []string
|
||||
// we cannot use the dashboard service directly due to circular dependencies,
|
||||
// so either use the search client if the feature is enabled or use the dashboard store
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
request := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Labels: []*resource.Requirement{},
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_FOLDER,
|
||||
Operator: string(selection.In),
|
||||
Values: folders,
|
||||
},
|
||||
},
|
||||
// We need a list of dashboard uids inside the folder to delete related dashboards & public dashboards -
|
||||
// we cannot use the dashboard service directly due to circular dependencies, so use the search client to get the dashboards
|
||||
request := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Labels: []*resource.Requirement{},
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_FOLDER,
|
||||
Operator: string(selection.In),
|
||||
Values: folders,
|
||||
},
|
||||
Limit: 100000}
|
||||
},
|
||||
},
|
||||
Limit: 100000}
|
||||
|
||||
res, err := s.dashboardK8sClient.Search(ctx, cmd.OrgID, request)
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||
}
|
||||
res, err := s.dashboardK8sClient.Search(ctx, cmd.OrgID, request)
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||
}
|
||||
|
||||
hits, err := dashboardsearch.ParseResults(res, 0)
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||
}
|
||||
dashboardUIDs = make([]string, len(hits.Hits))
|
||||
for i, dashboard := range hits.Hits {
|
||||
dashboardUIDs[i] = dashboard.Name
|
||||
err = s.dashboardK8sClient.Delete(ctx, dashboard.Name, cmd.OrgID, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to delete child dashboard: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dashes, err := s.dashboardStore.FindDashboards(ctx, &dashboards.FindPersistedDashboardsQuery{
|
||||
SignedInUser: cmd.SignedInUser,
|
||||
FolderUIDs: folders,
|
||||
OrgId: cmd.OrgID,
|
||||
Type: searchstore.TypeDashboard,
|
||||
})
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||
}
|
||||
dashboardUIDs = make([]string, len(dashes))
|
||||
for i, dashboard := range dashes {
|
||||
dashboardUIDs[i] = dashboard.UID
|
||||
err = s.dashboardStore.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{
|
||||
UID: dashboard.UID,
|
||||
OrgID: cmd.OrgID,
|
||||
})
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to delete child dashboard: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delete all public dashboards in the folders
|
||||
err = s.publicDashboardService.DeleteByDashboardUIDs(ctx, cmd.OrgID, dashboardUIDs)
|
||||
hits, err := dashboardsearch.ParseResults(res, 0)
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||
}
|
||||
dashboardUIDs := make([]string, len(hits.Hits))
|
||||
for i, dashboard := range hits.Hits {
|
||||
dashboardUIDs[i] = dashboard.Name
|
||||
err = s.dashboardK8sClient.Delete(ctx, dashboard.Name, cmd.OrgID, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to delete public dashboards: %w", err)
|
||||
return folder.ErrInternal.Errorf("failed to delete child dashboard: %w", err)
|
||||
}
|
||||
}
|
||||
// Delete all public dashboards in the folders
|
||||
err = s.publicDashboardService.DeleteByDashboardUIDs(ctx, cmd.OrgID, dashboardUIDs)
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to delete public dashboards: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = s.unifiedStore.Delete(ctx, folders, cmd.OrgID)
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
@@ -179,11 +180,11 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
}
|
||||
|
||||
featuresArr := []any{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2}
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders}
|
||||
features := featuremgmt.WithFeatures(featuresArr...)
|
||||
|
||||
dashboardStore := dashboards.NewFakeDashboardStore(t)
|
||||
k8sCli := client.NewK8sHandler(dualwrite.ProvideTestService(), request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService, nil)
|
||||
k8sCli := client.NewK8sHandler(dualwrite.ProvideTestService(), request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService, nil, sort.ProvideService())
|
||||
unifiedStore := ProvideUnifiedStore(k8sCli, userService)
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -206,6 +207,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
|
||||
publicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
|
||||
|
||||
fakeK8sClient := new(client.MockK8sHandler)
|
||||
folderService := &Service{
|
||||
log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"),
|
||||
unifiedStore: unifiedStore,
|
||||
@@ -216,7 +218,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
metrics: newFoldersMetrics(nil),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
k8sclient: k8sCli,
|
||||
dashboardStore: dashboardStore,
|
||||
dashboardK8sClient: fakeK8sClient,
|
||||
publicDashboardService: publicDashboardService,
|
||||
}
|
||||
|
||||
@@ -348,7 +350,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When deleting folder by uid should not return access denied error - ForceDeleteRules false", func(t *testing.T) {
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once()
|
||||
fakeK8sClient.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{Results: &resource.ResourceTable{}}, nil).Once()
|
||||
publicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
||||
@@ -362,6 +364,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
|
||||
t.Run("When deleting folder by uid, expectedForceDeleteRules as false, and dashboard Restore turned on should not return access denied error", func(t *testing.T) {
|
||||
folderService.features = featuremgmt.WithFeatures(append(featuresArr, featuremgmt.FlagDashboardRestore)...)
|
||||
fakeK8sClient.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{Results: &resource.ResourceTable{}}, nil).Once()
|
||||
|
||||
expectedForceDeleteRules := false
|
||||
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
||||
@@ -375,6 +378,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
|
||||
t.Run("When deleting folder by uid, expectedForceDeleteRules as true, and dashboard Restore turned on should not return access denied error", func(t *testing.T) {
|
||||
folderService.features = featuremgmt.WithFeatures(append(featuresArr, featuremgmt.FlagDashboardRestore)...)
|
||||
fakeK8sClient.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{Results: &resource.ResourceTable{}}, nil).Once()
|
||||
|
||||
expectedForceDeleteRules := true
|
||||
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
||||
@@ -526,7 +530,7 @@ func TestSearchFoldersFromApiServer(t *testing.T) {
|
||||
}
|
||||
service := Service{
|
||||
k8sclient: fakeK8sClient,
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders),
|
||||
unifiedStore: folderStore,
|
||||
}
|
||||
user := &user.SignedInUser{OrgID: 1}
|
||||
@@ -779,7 +783,7 @@ func TestDeleteFoldersFromApiServer(t *testing.T) {
|
||||
dashboardStore: dashboardStore,
|
||||
publicDashboardService: publicDashboardFakeService,
|
||||
registry: make(map[string]folder.RegistryService),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders),
|
||||
}
|
||||
user := &user.SignedInUser{OrgID: 1}
|
||||
ctx := identity.WithRequester(context.Background(), user)
|
||||
@@ -798,52 +802,22 @@ func TestDeleteFoldersFromApiServer(t *testing.T) {
|
||||
require.NoError(t, service.RegisterService(alertingStore))
|
||||
|
||||
t.Run("Should delete folder", func(t *testing.T) {
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once()
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{}).Return(nil).Once()
|
||||
dashboardK8sclient.On("Search", mock.Anything, int64(1), mock.Anything).Return(&resource.ResourceSearchResponse{Results: &resource.ResourceTable{}}, nil).Once()
|
||||
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
|
||||
UID: "uid",
|
||||
UID: "uid1",
|
||||
OrgID: 1,
|
||||
SignedInUser: user,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should delete dashboards and public dashboards within the folder", func(t *testing.T) {
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{
|
||||
{
|
||||
UID: "test",
|
||||
OrgID: 1,
|
||||
},
|
||||
{
|
||||
UID: "test2",
|
||||
OrgID: 1,
|
||||
},
|
||||
}, nil).Once()
|
||||
dashboardStore.On("DeleteDashboard", mock.Anything, &dashboards.DeleteDashboardCommand{
|
||||
UID: "test",
|
||||
OrgID: 1,
|
||||
}).Return(nil).Once()
|
||||
dashboardStore.On("DeleteDashboard", mock.Anything, &dashboards.DeleteDashboardCommand{
|
||||
UID: "test2",
|
||||
OrgID: 1,
|
||||
}).Return(nil).Once()
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"test", "test2"}).Return(nil).Once()
|
||||
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
|
||||
UID: "uid",
|
||||
OrgID: 1,
|
||||
SignedInUser: user,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
dashboardStore.AssertExpectations(t)
|
||||
dashboardK8sclient.AssertExpectations(t)
|
||||
publicDashboardFakeService.AssertExpectations(t)
|
||||
})
|
||||
|
||||
// enable k8s ff for dashboards, retest
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagKubernetesCliDashboards)
|
||||
|
||||
t.Run("Should delete dashboards and public dashboards within the folder through k8s if the ff is enabled", func(t *testing.T) {
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"uid1"}).Return(nil).Once()
|
||||
dashboardK8sclient.On("Delete", mock.Anything, "uid1", int64(1), mock.Anything).Return(nil).Once()
|
||||
t.Run("Should delete folders, dashboards, and public dashboards within the folder", func(t *testing.T) {
|
||||
fakeFolderStore.ExpectedFolders = []*folder.Folder{{UID: "uid2", ID: 2}}
|
||||
dashboardK8sclient.On("Delete", mock.Anything, "test", int64(1), mock.Anything).Return(nil).Once()
|
||||
dashboardK8sclient.On("Delete", mock.Anything, "test2", int64(1), mock.Anything).Return(nil).Once()
|
||||
dashboardK8sclient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Labels: []*resource.Requirement{},
|
||||
@@ -851,7 +825,7 @@ func TestDeleteFoldersFromApiServer(t *testing.T) {
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_FOLDER,
|
||||
Operator: string(selection.In),
|
||||
Values: []string{"uid1"},
|
||||
Values: []string{"uid", "uid2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -870,11 +844,21 @@ func TestDeleteFoldersFromApiServer(t *testing.T) {
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid1",
|
||||
Resource: "folder",
|
||||
Name: "test",
|
||||
Resource: "dashboard",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("folder1"),
|
||||
[]byte("uid"),
|
||||
[]byte(""),
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "test2",
|
||||
Resource: "dashboard",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("uid2"),
|
||||
[]byte(""),
|
||||
},
|
||||
},
|
||||
@@ -882,13 +866,14 @@ func TestDeleteFoldersFromApiServer(t *testing.T) {
|
||||
},
|
||||
TotalHits: 1,
|
||||
}, nil).Once()
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"test", "test2"}).Return(nil).Once()
|
||||
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
|
||||
UID: "uid1",
|
||||
UID: "uid",
|
||||
OrgID: 1,
|
||||
SignedInUser: user,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
dashboardStore.AssertExpectations(t)
|
||||
publicDashboardFakeService.AssertExpectations(t)
|
||||
dashboardK8sclient.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -468,7 +468,7 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
|
||||
if err := folderFilter.writeFolderFilterSQL(false, &builder); err != nil {
|
||||
return err
|
||||
}
|
||||
if query.SortDirection == search.SortAlphaDesc.Name {
|
||||
if query.SortDirection == sort.SortAlphaDesc.Name {
|
||||
builder.Write(" ORDER BY 1 DESC")
|
||||
} else {
|
||||
builder.Write(" ORDER BY 1 ASC")
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/kinds/librarypanel"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
)
|
||||
|
||||
func TestGetAllLibraryElements(t *testing.T) {
|
||||
@@ -281,7 +281,7 @@ func TestGetAllLibraryElements(t *testing.T) {
|
||||
|
||||
err := sc.reqContext.Req.ParseForm()
|
||||
require.NoError(t, err)
|
||||
sc.reqContext.Req.Form.Add("sortDirection", search.SortAlphaDesc.Name)
|
||||
sc.reqContext.Req.Form.Add("sortDirection", sort.SortAlphaDesc.Name)
|
||||
resp = sc.service.getAllHandler(sc.reqContext)
|
||||
require.Equal(t, 200, resp.Status())
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -348,7 +349,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
_, err = folderSvc.Create(context.Background(), &folder.CreateFolderCommand{UID: folderUID, SignedInUser: &user, Title: folderUID + "-title"})
|
||||
require.NoError(t, err)
|
||||
service, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||
@@ -364,6 +365,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
|
||||
nil,
|
||||
nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
service.RegisterDashboardPermissions(dashboardPermissions)
|
||||
@@ -387,7 +389,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc fold
|
||||
store := folderimpl.ProvideStore(sc.sqlStore)
|
||||
folderSvc = folderimpl.ProvideService(
|
||||
store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
t.Logf("Creating folder with title %q and UID uid_for_%s", title, title)
|
||||
}
|
||||
ctx := identity.WithRequester(context.Background(), &sc.user)
|
||||
@@ -451,12 +453,12 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashboardService, svcErr := dashboardservice.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore,
|
||||
features, folderPermissions, ac,
|
||||
folderSvc, fStore,
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, svcErr)
|
||||
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
@@ -520,7 +522,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
publicDash.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, features, sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
|
||||
require.NoError(t, err)
|
||||
err = folderSvc.RegisterService(alertStore)
|
||||
@@ -529,7 +531,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
cfg, dashboardStore, folderStore,
|
||||
features, folderPermissions, ac,
|
||||
folderSvc, fStore,
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, dashSvcErr)
|
||||
dashService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -737,7 +738,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash
|
||||
cfg, dashboardStore, folderStore,
|
||||
features, acmock.NewMockedPermissionsService(), ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
service.RegisterDashboardPermissions(dashPermissionService)
|
||||
@@ -759,7 +760,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
|
||||
fStore := folderimpl.ProvideStore(sc.sqlStore)
|
||||
s := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
t.Logf("Creating folder with title and UID %q", title)
|
||||
ctx := identity.WithRequester(context.Background(), sc.user)
|
||||
@@ -835,7 +836,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
cfg, dashStore, folderStore,
|
||||
features, acmock.NewMockedPermissionsService(), ac,
|
||||
folderSvc, folder.NewFakeStore(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashService.RegisterDashboardPermissions(dashPermissionService)
|
||||
@@ -847,7 +848,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
|
||||
folderService := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac, dashService)
|
||||
service := LibraryPanelService{
|
||||
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
@@ -1922,7 +1923,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderService := folderimpl.ProvideService(
|
||||
fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
store := store.DBstore{
|
||||
Logger: log,
|
||||
SQLStore: sqlStore,
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
@@ -1607,7 +1608,7 @@ func TestProvisiongWithFullpath(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderService := folderimpl.ProvideService(
|
||||
fStore, ac, inProcBus, dashboardStore, folderStore,
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
ruleService := createAlertRuleService(t, folderService)
|
||||
var orgID int64 = 1
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -33,7 +34,7 @@ func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStor
|
||||
tb.Helper()
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
return folderimpl.ProvideService(fStore, ac, bus, dashboardStore, folderStore, nil, db,
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
}
|
||||
|
||||
func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) {
|
||||
@@ -64,7 +65,9 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa
|
||||
cfg, dashboardStore, fs,
|
||||
features, folderPermissions, ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(),
|
||||
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
|
||||
dualwrite.ProvideTestService(),
|
||||
sort.ProvideService(),
|
||||
)
|
||||
require.NoError(tb, err)
|
||||
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
@@ -132,7 +133,7 @@ func TestDashboardFileReader(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
||||
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(),
|
||||
supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
t.Run("Reading dashboards from disk", func(t *testing.T) {
|
||||
t.Run("Can read default dashboard", func(t *testing.T) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
grafanasort "github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
@@ -51,7 +52,7 @@ func TestDuplicatesValidator(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
||||
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{}, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(),
|
||||
supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
supportbundlestest.NewFakeBundleService(), nil, cfgT, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), grafanasort.ProvideService())
|
||||
|
||||
t.Run("Duplicates validator should collect info about duplicate UIDs and titles within folders", func(t *testing.T) {
|
||||
const folderName = "duplicates-validator-folder"
|
||||
|
||||
@@ -42,6 +42,7 @@ import (
|
||||
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
@@ -328,7 +329,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
|
||||
cfg, dashboardStoreService, folderStore,
|
||||
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), ac,
|
||||
foldertest.NewFakeService(), folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil,
|
||||
nil, dualwrite.ProvideTestService(),
|
||||
nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashService.RegisterDashboardPermissions(dashPermissionService)
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2"
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -1398,10 +1399,9 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(testDB)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil,
|
||||
client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService())
|
||||
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{})
|
||||
fakeGuardian := &guardian.FakeDashboardGuardian{
|
||||
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore"
|
||||
secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
@@ -496,9 +497,9 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService())
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
require.NoError(t, err)
|
||||
dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService())
|
||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
grafanasort "github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/star"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -19,18 +20,15 @@ import (
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/search")
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, sqlstore db.DB, starService star.Service, dashboardService dashboards.DashboardService, folderService folder.Service, features featuremgmt.FeatureToggles) *SearchService {
|
||||
func ProvideService(cfg *setting.Cfg, sqlstore db.DB, starService star.Service, dashboardService dashboards.DashboardService, folderService folder.Service, features featuremgmt.FeatureToggles, sortService grafanasort.Service) *SearchService {
|
||||
s := &SearchService{
|
||||
Cfg: cfg,
|
||||
sortOptions: map[string]model.SortOption{
|
||||
SortAlphaAsc.Name: SortAlphaAsc,
|
||||
SortAlphaDesc.Name: SortAlphaDesc,
|
||||
},
|
||||
Cfg: cfg,
|
||||
sqlstore: sqlstore,
|
||||
starService: starService,
|
||||
folderService: folderService,
|
||||
features: features,
|
||||
dashboardService: dashboardService,
|
||||
sortService: sortService,
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -61,7 +59,7 @@ type Service interface {
|
||||
|
||||
type SearchService struct {
|
||||
Cfg *setting.Cfg
|
||||
sortOptions map[string]model.SortOption
|
||||
sortService grafanasort.Service
|
||||
sqlstore db.DB
|
||||
starService star.Service
|
||||
dashboardService dashboards.DashboardService
|
||||
@@ -109,7 +107,7 @@ func (s *SearchService) SearchHandler(ctx context.Context, query *Query) (model.
|
||||
IsDeleted: query.IsDeleted,
|
||||
}
|
||||
|
||||
if sortOpt, exists := s.sortOptions[query.Sort]; exists {
|
||||
if sortOpt, exists := s.sortService.GetSortOption(query.Sort); exists {
|
||||
dashboardQuery.Sort = sortOpt
|
||||
}
|
||||
|
||||
@@ -154,3 +152,11 @@ func sortedHits(unsorted model.HitList) model.HitList {
|
||||
|
||||
return hits
|
||||
}
|
||||
|
||||
func (s *SearchService) RegisterSortOption(option model.SortOption) {
|
||||
s.sortService.RegisterSortOption(option)
|
||||
}
|
||||
|
||||
func (s *SearchService) SortOptions() []model.SortOption {
|
||||
return s.sortService.SortOptions()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package search
|
||||
package sort
|
||||
|
||||
import (
|
||||
"sort"
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
)
|
||||
|
||||
// sort is separated into its own service to allow the dashboard service to use it in the k8s
|
||||
// fallback (see pkg/registry/apis/dashboard/legacysearcher/search_client.go), since search
|
||||
// has a direct dependency on the dashboard service (and thus would create a circular dependency in wire)
|
||||
|
||||
var (
|
||||
SortAlphaAsc = model.SortOption{
|
||||
Name: "alpha-asc",
|
||||
@@ -28,13 +32,26 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
sortOptions map[string]model.SortOption
|
||||
}
|
||||
|
||||
func ProvideService() Service {
|
||||
return Service{
|
||||
sortOptions: map[string]model.SortOption{
|
||||
SortAlphaAsc.Name: SortAlphaAsc,
|
||||
SortAlphaDesc.Name: SortAlphaDesc,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSortOption allows for hooking in more search options from
|
||||
// other services.
|
||||
func (s *SearchService) RegisterSortOption(option model.SortOption) {
|
||||
func (s *Service) RegisterSortOption(option model.SortOption) {
|
||||
s.sortOptions[option.Name] = option
|
||||
}
|
||||
|
||||
func (s *SearchService) SortOptions() []model.SortOption {
|
||||
func (s *Service) SortOptions() []model.SortOption {
|
||||
opts := make([]model.SortOption, 0, len(s.sortOptions))
|
||||
for _, o := range s.sortOptions {
|
||||
opts = append(opts, o)
|
||||
@@ -44,3 +61,8 @@ func (s *SearchService) SortOptions() []model.SortOption {
|
||||
})
|
||||
return opts
|
||||
}
|
||||
|
||||
func (s *Service) GetSortOption(sort string) (model.SortOption, bool) {
|
||||
option, ok := s.sortOptions[sort]
|
||||
return option, ok
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
@@ -825,7 +826,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db),
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
// create parent folder
|
||||
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
@@ -82,7 +83,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe
|
||||
fStore := folderimpl.ProvideStore(store)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store),
|
||||
nil, store, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, store, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||
|
||||
@@ -165,7 +165,7 @@ func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetS
|
||||
sb.Write(`(SELECT COUNT(DISTINCT (` + dialect.Quote("rule_group") + `)) FROM ` + dialect.Quote("alert_rule") + `) AS rule_groups,`)
|
||||
}
|
||||
// currently not supported when dashboards are in unified storage
|
||||
if !ss.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
if !ss.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
sb.Write(`(SELECT SUM(LENGTH(data)) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboard_bytes_total,`, dialect.BooleanStr(false))
|
||||
sb.Write(`(SELECT MAX(LENGTH(data)) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboard_bytes_max,`, dialect.BooleanStr(false))
|
||||
}
|
||||
|
||||
@@ -61,9 +61,14 @@ target = all
|
||||
protocol = https
|
||||
|
||||
[feature_toggles]
|
||||
; store folders in k8s
|
||||
kubernetesFolders = true
|
||||
grafanaAPIServerWithExperimentalAPIs = true
|
||||
kubernetesClientDashboardsFolders = true
|
||||
|
||||
[unified_storage.folders.folder.grafana.app]
|
||||
dualWriterMode = 4
|
||||
|
||||
[unified_storage.dashboards.dashboard.grafana.app]
|
||||
dualWriterMode = 4
|
||||
|
||||
[grafana-apiserver]
|
||||
; use unified storage for k8s apiserver
|
||||
|
||||
@@ -14,7 +14,7 @@ exclude k8s.io/client-go v12.0.0+incompatible
|
||||
require (
|
||||
github.com/bwmarrin/snowflake v0.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31
|
||||
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d
|
||||
@@ -192,7 +192,7 @@ require (
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038 // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc // indirect
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 // indirect
|
||||
github.com/grafana/dataplane/sdata v0.0.9 // indirect
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
|
||||
github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect
|
||||
|
||||
@@ -566,10 +566,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-20250207161551-04c87cf39038 h1:dG/UKAjY/KlKp9fY8aEm+gSQHHRmPm5q+9cea3hRSu8=
|
||||
github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc h1:qx8avmRi3wx+SuAn6UV0pKma1fJ0/4POUYNc+2rxZkg=
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc/go.mod h1:HamgITzYqErFp8qLwYM/JwwE3z6fXft0Z+5n0Dx1rUM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=
|
||||
github.com/grafana/dataplane/examples v0.0.1/go.mod h1:h5YwY8s407/17XF5/dS8XrUtsTVV2RnuW8+m1Mp46mg=
|
||||
github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s=
|
||||
|
||||
@@ -3,17 +3,18 @@ package apistore
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
authtypes "github.com/grafana/authlib/types"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/storage"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
)
|
||||
@@ -37,9 +38,9 @@ func formatBytes(numBytes int) string {
|
||||
|
||||
// Called on create
|
||||
func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime.Object) ([]byte, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
info, ok := authtypes.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("missing auth info")
|
||||
}
|
||||
|
||||
obj, err := utils.MetaAccessor(newObject)
|
||||
@@ -80,7 +81,7 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime
|
||||
obj.SetRepositoryInfo(repo)
|
||||
obj.SetUpdatedBy("")
|
||||
obj.SetUpdatedTimestamp(nil)
|
||||
obj.SetCreatedBy(user.GetUID())
|
||||
obj.SetCreatedBy(info.GetUID())
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = s.codec.Encode(newObject, &buf); err != nil {
|
||||
@@ -91,9 +92,9 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime
|
||||
|
||||
// Called on update
|
||||
func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runtime.Object, previousObject runtime.Object) ([]byte, error) {
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
info, ok := authtypes.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("missing auth info")
|
||||
}
|
||||
|
||||
obj, err := utils.MetaAccessor(updateObject)
|
||||
@@ -141,7 +142,7 @@ func (s *Storage) prepareObjectForUpdate(ctx context.Context, updateObject runti
|
||||
return nil, err
|
||||
}
|
||||
obj.SetRepositoryInfo(repo)
|
||||
obj.SetUpdatedBy(user.GetUID())
|
||||
obj.SetUpdatedBy(info.GetUID())
|
||||
obj.SetUpdatedTimestampMillis(time.Now().UnixMilli())
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/snowflake"
|
||||
authtypes "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/rand"
|
||||
"k8s.io/apimachinery/pkg/api/apitesting"
|
||||
@@ -32,12 +32,13 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
LargeObjectSupport: nil,
|
||||
},
|
||||
}
|
||||
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{UserID: 1, UserUID: "user-uid"})
|
||||
|
||||
t.Run("Error getting requester from context", func(t *testing.T) {
|
||||
ctx := authtypes.WithAuthInfo(context.Background(), &identity.StaticRequester{UserID: 1, UserUID: "user-uid", Type: authtypes.TypeUser})
|
||||
|
||||
t.Run("Error getting auth info from context", func(t *testing.T) {
|
||||
_, err := s.prepareObjectForStorage(context.Background(), nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "a Requester was not found in the context")
|
||||
require.Contains(t, err.Error(), "missing auth info")
|
||||
})
|
||||
|
||||
t.Run("Error on missing name", func(t *testing.T) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
ngalertstore "github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -53,7 +54,7 @@ func TestDirectSQLStats(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db),
|
||||
nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService())
|
||||
nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
|
||||
// create parent folder
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ func (x *searchableDocumentFields) Fields() []string {
|
||||
}
|
||||
|
||||
func (x *searchableDocumentFields) Field(name string) *ResourceTableColumnDefinition {
|
||||
name = strings.TrimPrefix(name, "fields.")
|
||||
name = strings.TrimPrefix(name, SEARCH_FIELD_PREFIX)
|
||||
|
||||
f, ok := x.fields[name]
|
||||
if ok {
|
||||
@@ -246,6 +246,7 @@ func (x *searchableDocumentFields) Field(name string) *ResourceTableColumnDefini
|
||||
return nil
|
||||
}
|
||||
|
||||
const SEARCH_FIELD_PREFIX = "fields."
|
||||
const SEARCH_FIELD_ID = "_id" // {namespace}/{group}/{resource}/{name}
|
||||
const SEARCH_FIELD_KIND = "kind" // resource ( for federated index filtering )
|
||||
const SEARCH_FIELD_GROUP_RESOURCE = "gr" // group/resource
|
||||
|
||||
@@ -11,8 +11,8 @@ replace (
|
||||
require (
|
||||
github.com/fullstorydev/grpchan v1.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040
|
||||
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.266.0
|
||||
|
||||
@@ -407,10 +407,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038 h1:dG/UKAjY/KlKp9fY8aEm+gSQHHRmPm5q+9cea3hRSu8=
|
||||
github.com/grafana/alerting v0.0.0-20250207161551-04c87cf39038/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU=
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc h1:qx8avmRi3wx+SuAn6UV0pKma1fJ0/4POUYNc+2rxZkg=
|
||||
github.com/grafana/authlib v0.0.0-20250217135712-106ec5baf6bc/go.mod h1:HamgITzYqErFp8qLwYM/JwwE3z6fXft0Z+5n0Dx1rUM=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo=
|
||||
github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7 h1:NTMmow+74I3Jb033xhbRgWQS7A//5TDhiM4tl7bsVP4=
|
||||
github.com/grafana/authlib v0.0.0-20250219100139-6a3b1bbb50e7/go.mod h1:T3X4z0ejGfJOiOmZLFeKCRT/yxWJq/RtclAc/PHj/w4=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31 h1:EokLC5grHwLPs4tXW8T6E8187H1e5G9AP0QQ5B60HbA=
|
||||
github.com/grafana/authlib/types v0.0.0-20250219092154-21ce22b49f31/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
|
||||
github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s=
|
||||
github.com/grafana/dataplane/sdata v0.0.9/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU=
|
||||
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 h1:IR+UNYHqaU31t8/TArJk8K/GlDwOyxMpGNkWCXeZ28g=
|
||||
|
||||
@@ -552,7 +552,7 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resource.Res
|
||||
fields := make([]string, 0, len(req.Fields))
|
||||
for _, f := range req.Fields {
|
||||
if slices.Contains(DashboardFields(), f) {
|
||||
f = "fields." + f
|
||||
f = resource.SEARCH_FIELD_PREFIX + f
|
||||
}
|
||||
fields = append(fields, f)
|
||||
}
|
||||
@@ -677,7 +677,7 @@ func getSortFields(req *resource.ResourceSearchRequest) []string {
|
||||
}
|
||||
|
||||
if slices.Contains(DashboardFields(), input) {
|
||||
input = "fields." + input
|
||||
input = resource.SEARCH_FIELD_PREFIX + input
|
||||
}
|
||||
|
||||
if sort.Desc {
|
||||
@@ -847,7 +847,7 @@ func (b *bleveIndex) hitsToTable(ctx context.Context, selectFields []string, hit
|
||||
v := match.Fields[fieldName]
|
||||
// fields that are specific to the resource get stored as fields.<fieldName>, so we need to check for that
|
||||
if v == nil {
|
||||
v = match.Fields["fields."+fieldName]
|
||||
v = match.Fields[resource.SEARCH_FIELD_PREFIX+fieldName]
|
||||
}
|
||||
if v != nil {
|
||||
// Encode the value to protobuf
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -123,7 +123,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
},
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
},
|
||||
}))
|
||||
})
|
||||
@@ -139,7 +139,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
},
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
},
|
||||
}))
|
||||
})
|
||||
@@ -155,7 +155,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
},
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
featuremgmt.FlagNestedFolders,
|
||||
},
|
||||
}))
|
||||
@@ -172,7 +172,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
},
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
featuremgmt.FlagNestedFolders,
|
||||
},
|
||||
}))
|
||||
@@ -189,7 +189,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
},
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
featuremgmt.FlagNestedFolders,
|
||||
},
|
||||
}))
|
||||
@@ -206,7 +206,7 @@ func TestIntegrationFoldersApp(t *testing.T) {
|
||||
},
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
featuremgmt.FlagNestedFolders,
|
||||
},
|
||||
}))
|
||||
@@ -577,7 +577,7 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) {
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagNestedFolders,
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -679,7 +679,7 @@ func TestIntegrationFolderGetPermissions(t *testing.T) {
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagNestedFolders,
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -856,7 +856,7 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagNestedFolders,
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1026,7 +1026,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) {
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagNestedFolders,
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ func TestIntegrationOpenAPIs(t *testing.T) {
|
||||
h := NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2, // Will be default on by G12
|
||||
featuremgmt.FlagQueryService, // Query Library
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders, // Will be default on by G12
|
||||
featuremgmt.FlagQueryService, // Query Library
|
||||
featuremgmt.FlagProvisioning,
|
||||
featuremgmt.FlagInvestigationsBackend,
|
||||
},
|
||||
|
||||
@@ -48,9 +48,8 @@ func TestIntegrationProvisioning(t *testing.T) {
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagProvisioning,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
||||
featuremgmt.FlagKubernetesCliDashboards,
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagUnifiedStorageSearch,
|
||||
featuremgmt.FlagKubernetesClientDashboardsFolders,
|
||||
},
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
"dashboards.dashboard.grafana.app": {
|
||||
|
||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user