diff --git a/.betterer.results b/.betterer.results index 3ec225ceb44..12d1729d2e1 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5,7 +5,7 @@ // exports[`better eslint`] = { value: `{ - "e2e/scenes/utils/support/types.ts:5381": [ + "e2e/old-arch/utils/support/types.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], "e2e/utils/support/types.ts:5381": [ @@ -1655,8 +1655,7 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "3"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], "public/app/features/alerting/unified/components/contact-points/components/ContactPointsFilter.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] @@ -1766,12 +1765,6 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], - "public/app/features/alerting/unified/components/receivers/DuplicateTemplateView.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], - "public/app/features/alerting/unified/components/receivers/EditTemplateView.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/alerting/unified/components/receivers/GlobalConfigForm.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -1895,8 +1888,7 @@ exports[`better eslint`] = { ], "public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], "public/app/features/alerting/unified/components/rule-editor/CloudAlertPreview.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -2091,8 +2083,7 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "2"], [0, 0, 0, "No untranslated strings. Wrap text with ", "3"], [0, 0, 0, "No untranslated strings. Wrap text with ", "4"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "5"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "6"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "5"] ], "public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/SmartAlertTypeDetector.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], @@ -2776,6 +2767,9 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], @@ -2785,6 +2779,10 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] ], + "public/app/features/dashboard-scene/scene/types.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + ], "public/app/features/dashboard-scene/serialization/angularMigration.test.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], @@ -7228,7 +7226,7 @@ exports[`no undocumented stories`] = { exports[`no gf-form usage`] = { value: `{ - "e2e/scenes/utils/flows/addDataSource.ts:5381": [ + "e2e/old-arch/utils/flows/addDataSource.ts:5381": [ [0, 0, 0, "gf-form usage has been deprecated. Use a component from @grafana/ui or custom CSS instead.", "5381"] ], "e2e/utils/flows/addDataSource.ts:5381": [ diff --git a/.drone.yml b/.drone.yml index 09e7ec86cc7..207e0d20ecb 100644 --- a/.drone.yml +++ b/.drone.yml @@ -18,7 +18,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -69,7 +69,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -120,7 +120,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -178,7 +178,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -279,7 +279,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -367,7 +367,7 @@ steps: name: clone-enterprise - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -454,7 +454,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -543,11 +543,11 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -648,7 +648,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.19.1 + image: alpine:3.20.3 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -660,14 +660,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-dashboards-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/dashboards-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/dashboards-suite + name: end-to-end-tests-old-arch/dashboards-suite - commands: - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite depends_on: @@ -678,14 +678,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-smoke-tests-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/smoke-tests-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/smoke-tests-suite + name: end-to-end-tests-old-arch/smoke-tests-suite - commands: - ./bin/build e2e-tests --port 3001 --suite panels-suite depends_on: @@ -696,14 +696,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-panels-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/panels-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/panels-suite + name: end-to-end-tests-old-arch/panels-suite - commands: - ./bin/build e2e-tests --port 3001 --suite various-suite depends_on: @@ -714,14 +714,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-various-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/various-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/various-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/various-suite + name: end-to-end-tests-old-arch/various-suite - commands: - cd / - ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH} @@ -868,7 +868,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ + --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.3 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -1003,7 +1003,7 @@ steps: name: clone-enterprise - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -1016,7 +1016,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1205,7 +1205,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1562,7 +1562,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1638,7 +1638,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1696,7 +1696,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1762,7 +1762,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -1842,7 +1842,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -1908,7 +1908,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -1968,11 +1968,11 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2072,7 +2072,7 @@ steps: GF_APP_MODE: development GF_SERVER_HTTP_PORT: "3001" GF_SERVER_ROUTER_LOGGING: "1" - image: alpine:3.19.1 + image: alpine:3.20.3 name: grafana-server - commands: - ./bin/build e2e-tests --port 3001 --suite dashboards-suite @@ -2084,14 +2084,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-dashboards-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/dashboards-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/dashboards-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/dashboards-suite + name: end-to-end-tests-old-arch/dashboards-suite - commands: - ./bin/build e2e-tests --port 3001 --suite smoke-tests-suite depends_on: @@ -2102,14 +2102,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-smoke-tests-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/smoke-tests-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/smoke-tests-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/smoke-tests-suite + name: end-to-end-tests-old-arch/smoke-tests-suite - commands: - ./bin/build e2e-tests --port 3001 --suite panels-suite depends_on: @@ -2120,14 +2120,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-panels-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/panels-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/panels-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/panels-suite + name: end-to-end-tests-old-arch/panels-suite - commands: - ./bin/build e2e-tests --port 3001 --suite various-suite depends_on: @@ -2138,14 +2138,14 @@ steps: image: cypress/included:13.10.0 name: end-to-end-tests-various-suite - commands: - - ./bin/build e2e-tests --port 3001 --suite scenes/various-suite + - ./bin/build e2e-tests --port 3001 --suite old-arch/various-suite depends_on: - grafana-server - build-test-plugins environment: HOST: grafana-server image: cypress/included:13.10.0 - name: end-to-end-tests-scenes/various-suite + name: end-to-end-tests-old-arch/various-suite - commands: - cd / - ./cpp-e2e/scripts/ci-run.sh azure ${DRONE_SOURCE_BRANCH} @@ -2328,7 +2328,7 @@ steps: - /src/grafana-build artifacts -a docker:grafana:linux/amd64 -a docker:grafana:linux/amd64:ubuntu -a docker:grafana:linux/arm64 -a docker:grafana:linux/arm64:ubuntu -a docker:grafana:linux/arm/v7 -a docker:grafana:linux/arm/v7:ubuntu --yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER - --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.19.1 --tag-format='{{ + --go-version=1.23.1 --ubuntu-base=ubuntu:22.04 --alpine-base=alpine:3.20.3 --tag-format='{{ .version_base }}-{{ .buildID }}-{{ .arch }}' --grafana-dir=$$PWD --ubuntu-tag-format='{{ .version_base }}-{{ .buildID }}-ubuntu-{{ .arch }}' > docker.txt - find ./dist -name '*docker*.tar.gz' -type f | xargs -n1 docker load -i @@ -2525,7 +2525,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -2538,7 +2538,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -2730,7 +2730,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -2856,7 +2856,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2912,7 +2912,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -2976,7 +2976,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3054,7 +3054,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd @@ -3157,7 +3157,7 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3170,7 +3170,7 @@ steps: name: compile-build-cmd - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -3398,11 +3398,11 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3529,11 +3529,11 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl @@ -3868,14 +3868,14 @@ steps: - echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list - 'echo "Step 5: Installing Grafana..."' - - for i in $(seq 1 10); do + - for i in $(seq 1 60); do - ' if apt-get update >/dev/null 2>&1 && DEBIAN_FRONTEND=noninteractive apt-get install -yq grafana=${TAG} >/dev/null 2>&1; then' - ' echo "Command succeeded on attempt $i"' - ' break' - ' else' - ' echo "Attempt $i failed"' - - ' if [ $i -eq 10 ]; then' + - ' if [ $i -eq 60 ]; then' - ' echo ''All attempts failed''' - ' exit 1' - ' fi' @@ -3918,13 +3918,13 @@ steps: - dnf list available grafana-${TAG} - if [ $? -eq 0 ]; then - ' echo "Grafana package found in repository. Installing from repo..."' - - for i in $(seq 1 5); do + - for i in $(seq 1 60); do - ' if dnf install -y --nogpgcheck grafana-${TAG} >/dev/null 2>&1; then' - ' echo "Command succeeded on attempt $i"' - ' break' - ' else' - ' echo "Attempt $i failed"' - - ' if [ $i -eq 5 ]; then' + - ' if [ $i -eq 60 ]; then' - ' echo ''All attempts failed''' - ' exit 1' - ' fi' @@ -4045,14 +4045,14 @@ steps: - echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list - 'echo "Step 5: Installing Grafana..."' - - for i in $(seq 1 10); do + - for i in $(seq 1 60); do - ' if apt-get update >/dev/null 2>&1 && DEBIAN_FRONTEND=noninteractive apt-get install -yq grafana=${TAG} >/dev/null 2>&1; then' - ' echo "Command succeeded on attempt $i"' - ' break' - ' else' - ' echo "Attempt $i failed"' - - ' if [ $i -eq 10 ]; then' + - ' if [ $i -eq 60 ]; then' - ' echo ''All attempts failed''' - ' exit 1' - ' fi' @@ -4096,13 +4096,13 @@ steps: - dnf list available grafana-${TAG} - if [ $? -eq 0 ]; then - ' echo "Grafana package found in repository. Installing from repo..."' - - for i in $(seq 1 5); do + - for i in $(seq 1 60); do - ' if dnf install -y --nogpgcheck grafana-${TAG} >/dev/null 2>&1; then' - ' echo "Command succeeded on attempt $i"' - ' break' - ' else' - ' echo "Attempt $i failed"' - - ' if [ $i -eq 5 ]; then' + - ' if [ $i -eq 60 ]; then' - ' echo ''All attempts failed''' - ' exit 1' - ' fi' @@ -4165,6 +4165,49 @@ volumes: path: /var/run/docker.sock name: docker --- +clone: + retries: 3 +depends_on: [] +image_pull_secrets: +- gcr +- gar +kind: pipeline +name: publish-grafanacom +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] + environment: + CGO_ENABLED: 0 + image: golang:1.23.1-alpine + name: compile-build-cmd +- commands: + - ./bin/build publish grafana-com --edition oss ${DRONE_TAG} + depends_on: + - compile-build-cmd + environment: + GCP_KEY: + from_secret: gcp_grafanauploads_base64 + GRAFANA_COM_API_KEY: + from_secret: grafana_api_key + image: grafana/grafana-ci-deploy:1.3.3 + name: publish-grafanacom +trigger: + event: + - promote + target: publish-grafanacom +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- clone: retries: 3 depends_on: @@ -4188,7 +4231,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4305,7 +4348,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -4361,7 +4404,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4443,7 +4486,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4514,7 +4557,7 @@ steps: name: identify-runner - commands: - $$ProgressPreference = "SilentlyContinue" - - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/windows/grabpl.exe + - Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/windows/grabpl.exe -OutFile grabpl.exe image: grafana/ci-wix:0.1.1 name: windows-init @@ -4626,7 +4669,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -4728,7 +4771,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - yarn install --immutable || yarn install --immutable @@ -4782,7 +4825,7 @@ services: [] steps: - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -4862,7 +4905,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5009,7 +5052,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5119,7 +5162,7 @@ steps: environment: _EXPERIMENTAL_DAGGER_CLOUD_TOKEN: from_secret: dagger_token - ALPINE_BASE: alpine:3.19.1 + ALPINE_BASE: alpine:3.20.3 CDN_DESTINATION: from_secret: rgm_cdn_destination DESTINATION: @@ -5316,13 +5359,13 @@ services: steps: - commands: - mkdir -p bin - - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.50/grabpl + - curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.53/grabpl - chmod +x bin/grabpl image: byrnedo/alpine-curl:0.1.8 name: grabpl - commands: - echo $DRONE_RUNNER_NAME - image: alpine:3.19.1 + image: alpine:3.20.3 name: identify-runner - commands: - '# It is required that code generated from Thema/CUE be committed and in sync @@ -5820,7 +5863,7 @@ steps: - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:20-bookworm - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.19.1 + - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine:3.20.3 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM ubuntu:22.04 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM byrnedo/alpine-curl:0.1.8 - trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack @@ -5857,7 +5900,7 @@ steps: - trivy --exit-code 1 --severity HIGH,CRITICAL node:20-bookworm - trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0 - trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3 - - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.19.1 + - trivy --exit-code 1 --severity HIGH,CRITICAL alpine:3.20.3 - trivy --exit-code 1 --severity HIGH,CRITICAL ubuntu:22.04 - trivy --exit-code 1 --severity HIGH,CRITICAL byrnedo/alpine-curl:0.1.8 - trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack @@ -6108,6 +6151,6 @@ kind: secret name: gcr_credentials --- kind: signature -hmac: 7335b2e56769f72716f5dac524741e423abb99eacf775fa635e59c2d658c8aee +hmac: 766cd43d479f82bdb5bbaa3b48ed87ad13ea71d3418deb5d0c89ec7b77ae0475 ... diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c30c40ee279..9e0a8720582 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -80,7 +80,16 @@ /pkg/apis/query @grafana/grafana-datasources-core-services /pkg/bus/ @grafana/grafana-search-and-storage /pkg/cmd/ @grafana/grafana-backend-group -/pkg/cmd/grafana/apiserver @grafana/grafana-app-platform-squad +/pkg/cmd/grafana-cli/commands/install_command.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/install_command_test.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/listremote_command.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/listversions_command.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/ls_command_test.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/ls_command.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/remove_command.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/upgrade_command.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/upgrade_all_command.go @grafana/plugins-platform-backend +/pkg/cmd/grafana-cli/commands/upgrade_all_command_test.go @grafana/plugins-platform-backend /pkg/components/apikeygen/ @grafana/identity-squad /pkg/components/satokengen/ @grafana/identity-squad /pkg/components/dashdiffs/ @grafana/grafana-app-platform-squad @@ -440,7 +449,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/features/invites/ @grafana/grafana-frontend-platform /public/app/features/library-panels/ @grafana/dashboards-squad /public/app/features/logs/ @grafana/observability-logs -/public/app/features/live/ @grafana/grafana-app-platform-squad +/public/app/features/live/ @grafana/dashboards-squad /public/app/features/apiserver/ @grafana/grafana-app-platform-squad /public/app/features/manage-dashboards/ @grafana/dashboards-squad /public/app/features/notifications/ @grafana/grafana-frontend-platform @@ -492,7 +501,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/plugins/panel/geomap/ @grafana/dataviz-squad /public/app/plugins/panel/canvas/ @grafana/dataviz-squad /public/app/plugins/panel/candlestick/ @grafana/dataviz-squad -/public/app/plugins/panel/live/ @grafana/grafana-app-platform-squad +/public/app/plugins/panel/live/ @grafana/dashboards-squad /public/app/plugins/panel/news/ @grafana/grafana-frontend-platform /public/app/plugins/panel/stat/ @grafana/dataviz-squad /public/app/plugins/panel/text/ @grafana/grafana-frontend-platform @@ -737,7 +746,6 @@ embed.go @grafana/grafana-as-code /.github/workflows/i18n-crowdin-download.yml @grafana/grafana-frontend-platform /.github/workflows/pr-go-workspace-check.yml @grafana/grafana-app-platform-squad /.github/workflows/pr-k8s-codegen-check.yml @grafana/grafana-app-platform-squad -/.github/workflows/run-scenes-e2e.yml @grafana/dashboards-squad /.github/workflows/go_lint.yml @grafana/grafana-backend-services-squad /.github/workflows/trivy-scan.yml @grafana/grafana-backend-services-squad /.github/workflows/changelog.yml @zserge diff --git a/.github/commands.json b/.github/commands.json index fdf90eb4722..af047be4341 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -475,14 +475,6 @@ "url": "https://github.com/orgs/grafana/projects/665" } }, - { - "type": "label", - "name": "area/dashboard", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "type/build-packaging", @@ -507,14 +499,6 @@ "url": "https://github.com/orgs/grafana/projects/56" } }, - { - "type": "label", - "name": "area/dashboard/templating", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "area/auth", @@ -579,14 +563,6 @@ "url": "https://github.com/orgs/grafana/projects/203" } }, - { - "type": "label", - "name": "area/dashboards/panel", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "area/public-dashboards", @@ -635,14 +611,6 @@ "url": "https://github.com/orgs/grafana/projects/112" } }, - { - "type": "label", - "name": "area/dashboard/data-links", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "area/backend/api", @@ -675,22 +643,6 @@ "url": "https://github.com/orgs/grafana/projects/221" } }, - { - "type": "label", - "name": "area/dashboard/snapshot", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, - { - "type": "label", - "name": "area/dashboard/folders", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "area/configuration", @@ -715,14 +667,6 @@ "url": "https://github.com/orgs/grafana/projects/660" } }, - { - "type": "label", - "name": "area/dashboard/timerange", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "datasource/MSSQL", @@ -827,6 +771,174 @@ "url": "https://github.com/orgs/grafana/projects/202" } }, + { + "type": "label", + "name": "area/dashboard/links", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/templating", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/variables", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboards/panel", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/data-links", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/rows", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/edit", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/panel/edit", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/scenes", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/snapshot", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/folders", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/timerange", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/repeating", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/annotations", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/library-panel", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/kiosk", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/tv", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/import", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/panel/field-override", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/panel/repeat", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, + { + "type": "label", + "name": "area/dashboard/settings", + "action": "addToProject", + "addToProject": { + "url": "https://github.com/orgs/grafana/projects/202" + } + }, { "type": "label", "name": "area/backend/db/sqlite", @@ -859,14 +971,6 @@ "url": "https://github.com/orgs/grafana/projects/78" } }, - { - "type": "label", - "name": "area/dashboard/links", - "action": "addToProject", - "addToProject": { - "url": "https://github.com/orgs/grafana/projects/202" - } - }, { "type": "label", "name": "datasource/Zipkin", diff --git a/.github/workflows/run-scenes-e2e.yml b/.github/workflows/run-scenes-e2e.yml deleted file mode 100644 index 53425680492..00000000000 --- a/.github/workflows/run-scenes-e2e.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Run dashboard scenes e2e - -on: - schedule: - - cron: "0 8 * * 1-5" # every day at 08:00UTC on weekdays - # push # uncomment for test run during PR - -env: - ARCH: linux-amd64 - -jobs: - dashboard-scenes-e2e: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Pin Go version to mod file - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - run: go version - - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install dependencies - run: yarn install --immutable - - name: Build grafana - run: make build - - name: Install Cypress dependencies - uses: cypress-io/github-action@v6 - with: - runTests: false - - name: Run dashboard scenes e2e - run: yarn e2e:scenes - - name: "Send Slack notification" - if: ${{ failure() }} - uses: slackapi/slack-github-action@v1.26.0 - with: - payload: > - { - "icon_emoji": ":this-is-fine-fire:", - "username": "Dashboard scenes e2e tests failed", - "text": "Link to run: https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}", - "channel": "#grafana-dashboards-squad" - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a7201ada6..b1ad791c62d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,92 @@ + + +# 11.2.2 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93353](https://github.com/grafana/grafana/pull/93353), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) +- **Data sources:** Hide the datasource redirection banner for users who can't interact with data sources [#93103](https://github.com/grafana/grafana/pull/93103), [@IevaVasiljeva](https://github.com/IevaVasiljeva) + +### Bug fixes + +- **Alerting:** Fix preview of silences when label name contains spaces [#93051](https://github.com/grafana/grafana/pull/93051), [@tomratcliffe](https://github.com/tomratcliffe) +- **Alerting:** Make query wrapper match up datasource UIDs if necessary [#93114](https://github.com/grafana/grafana/pull/93114), [@tomratcliffe](https://github.com/tomratcliffe) +- **AzureMonitor:** Deduplicate resource picker rows [#93705](https://github.com/grafana/grafana/pull/93705), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Improve resource picker efficiency [#93440](https://github.com/grafana/grafana/pull/93440), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Remove Basic Logs retention warning [#93123](https://github.com/grafana/grafana/pull/93123), [@aangelisc](https://github.com/aangelisc) +- **CloudWatch:** Fix segfault when migrating legacy queries [#93544](https://github.com/grafana/grafana/pull/93544), [@iwysiu](https://github.com/iwysiu) +- **Correlations:** Limit access to correlations page to users who can access Explore [#93676](https://github.com/grafana/grafana/pull/93676), [@ifrost](https://github.com/ifrost) +- **DashboardScene:** Fix broken error handling and error rendering [#93690](https://github.com/grafana/grafana/pull/93690), [@torkelo](https://github.com/torkelo) +- **Plugins:** Avoid returning 404 for `AutoEnabled` apps [#93488](https://github.com/grafana/grafana/pull/93488), [@wbrowne](https://github.com/wbrowne) + + + + +# 11.1.7 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93355](https://github.com/grafana/grafana/pull/93355), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) + +### Bug fixes + +- **Alerting:** Fix preview of silences when label name contains spaces [#93050](https://github.com/grafana/grafana/pull/93050), [@tomratcliffe](https://github.com/tomratcliffe) +- **Alerting:** Make query wrapper match up datasource UIDs if necessary [#93115](https://github.com/grafana/grafana/pull/93115), [@tomratcliffe](https://github.com/tomratcliffe) +- **AzureMonitor:** Deduplicate resource picker rows [#93704](https://github.com/grafana/grafana/pull/93704), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Improve resource picker efficiency [#93439](https://github.com/grafana/grafana/pull/93439), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Remove Basic Logs retention warning [#93122](https://github.com/grafana/grafana/pull/93122), [@aangelisc](https://github.com/aangelisc) +- **Correlations:** Limit access to correlations page to users who can access Explore [#93675](https://github.com/grafana/grafana/pull/93675), [@ifrost](https://github.com/ifrost) +- **Plugins:** Avoid returning 404 for `AutoEnabled` apps [#93487](https://github.com/grafana/grafana/pull/93487), [@wbrowne](https://github.com/wbrowne) + + + + +# 11.0.6 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93358](https://github.com/grafana/grafana/pull/93358), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) + +### Bug fixes + +- **AzureMonitor:** Deduplicate resource picker rows [#93703](https://github.com/grafana/grafana/pull/93703), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Improve resource picker efficiency [#93438](https://github.com/grafana/grafana/pull/93438), [@aangelisc](https://github.com/aangelisc) +- **Correlations:** Limit access to correlations page to users who can access Explore [#93674](https://github.com/grafana/grafana/pull/93674), [@ifrost](https://github.com/ifrost) +- **Plugins:** Avoid returning 404 for `AutoEnabled` apps [#93486](https://github.com/grafana/grafana/pull/93486), [@wbrowne](https://github.com/wbrowne) + + + + +# 10.4.10 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93359](https://github.com/grafana/grafana/pull/93359), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) + +### Bug fixes + +- **AzureMonitor:** Deduplicate resource picker rows [#93702](https://github.com/grafana/grafana/pull/93702), [@aangelisc](https://github.com/aangelisc) +- **Correlations:** Limit access to correlations page to users who can access Explore [#93673](https://github.com/grafana/grafana/pull/93673), [@ifrost](https://github.com/ifrost) + + + + +# 10.3.11 (2024-10-01) + +### Features and enhancements + +- **Chore:** Bump Go to 1.22.7 [#93360](https://github.com/grafana/grafana/pull/93360), [@hairyhenderson](https://github.com/hairyhenderson) +- **Chore:** Bump Go to 1.22.7 (Enterprise) + +### Bug fixes + +- **Correlations:** Limit access to correlations page to users who can access Explore [#93672](https://github.com/grafana/grafana/pull/93672), [@ifrost](https://github.com/ifrost) + + # 11.2.1 (2024-09-26) @@ -21,6 +110,7 @@ - **Reporting:** Disable dashboardSceneSolo when rendering PDFs the old way (Enterprise) - **Templating:** Fix searching non-latin template variables [#92893](https://github.com/grafana/grafana/pull/92893), [@leeoniya](https://github.com/leeoniya) - **TutorialCard:** Fix link to tutorial not opening [#92647](https://github.com/grafana/grafana/pull/92647), [@eledobleefe](https://github.com/eledobleefe) +- **Alerting:** Fixed CVE-2024-8118. ### Plugin development fixes & changes @@ -43,6 +133,7 @@ ### Plugin development fixes & changes - **Bugfix:** QueryField typeahead missing background color [#92316](https://github.com/grafana/grafana/pull/92316), [@mckn](https://github.com/mckn) +- **Alerting:** Fixed CVE-2024-8118. @@ -57,6 +148,7 @@ - **Provisioning:** Prevent provisioning folder errors from failing startup [#92588](https://github.com/grafana/grafana/pull/92588), [@suntala](https://github.com/suntala) - **TutorialCard:** Fix link to tutorial not opening [#92645](https://github.com/grafana/grafana/pull/92645), [@eledobleefe](https://github.com/eledobleefe) +- **Alerting:** Fixed CVE-2024-8118. @@ -70,12 +162,17 @@ ### Bug fixes - **Provisioning:** Prevent provisioning folder errors from failing startup [#92591](https://github.com/grafana/grafana/pull/92591), [@suntala](https://github.com/suntala) +- **Alerting:** Fixed CVE-2024-8118. # 10.3.10 (2024-09-26) +### Bug fixes + +- **Alerting:** Fixed CVE-2024-8118. + diff --git a/conf/defaults.ini b/conf/defaults.ini index 3e7fc20ef90..8baa6296110 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -582,6 +582,9 @@ oauth_auto_login = false # OAuth state max age cookie duration in seconds. Defaults to 600 seconds. oauth_state_cookie_max_age = 600 +# Sets a custom oAuth error message. This is useful if you need to point the users to a specific location for support. +oauth_login_error_message = oauth.login.error + # Minimum wait time in milliseconds for the server lock retry mechanism. # The server lock retry mechanism is used to prevent multiple Grafana instances from # simultaneously refreshing OAuth tokens. This mechanism waits at least this amount @@ -615,6 +618,10 @@ id_response_header_prefix = X-Grafana # The header value will encode the namespace ("user:", "api-key:", "service-account:") id_response_header_namespaces = user api-key service-account +# Enables the use of managed service accounts for plugin authentication +# This feature currently **only supports single-organization deployments** +managed_service_accounts_enabled = false + #################################### SSO Settings ########################### [sso_settings] # interval for reloading the SSO Settings from the database diff --git a/conf/sample.ini b/conf/sample.ini index a36d17ff618..099de883121 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -583,6 +583,9 @@ # Deprecated, use auto_login option for specific provider instead. ;oauth_auto_login = false +# Sets a custom oAuth error message. This is useful if you need to point the users to a specific location for support. +;oauth_login_error_message = oauth.login.error + # OAuth state max age cookie duration in seconds. Defaults to 600 seconds. ;oauth_state_cookie_max_age = 600 @@ -619,6 +622,10 @@ # The header value will encode the namespace ("user:", "api-key:", "service-account:") ;id_response_header_namespaces = user api-key service-account +# Enables the use of managed service accounts for plugin authentication +# This feature currently **only supports single-organization deployments** +; managed_service_accounts_enabled = false + #################################### Anonymous Auth ###################### [auth.anonymous] # enable anonymous access diff --git a/docs/sources/administration/correlations/_index.md b/docs/sources/administration/correlations/_index.md index 977849d2948..3a61b1800b5 100644 --- a/docs/sources/administration/correlations/_index.md +++ b/docs/sources/administration/correlations/_index.md @@ -12,16 +12,18 @@ weight: 900 # Correlations -You can create interactive links for Explore visualizations to run queries related to presented data by setting up Correlations. +You can create interactive links for Explore visualizations by setting up Correlations. These links can either run queries or generate external URLs related to presented data. -A correlation defines how data in one [data source]({{< relref "../../datasources" >}}) is used to query data in another data source. +A correlation defines how data in one [data source]({{< relref "../../datasources" >}}) is used to query data in another data source or to generate an external URL. Some examples: - an application name returned in a logs data source can be used to query metrics related to that application in a metrics data source, or - a user name returned by an SQL data source can be used to query logs related to that particular user in a logs data source +- a customer ID in a logs data source can link to a different platform that has a profile on that customer. [Explore]({{< relref "../../explore" >}}) takes user-defined correlations to display links inside the visualizations. -You can click on a link to run the related query and see results in [Explore Split View]({{< relref "../../explore#split-and-compare" >}}). +If a correlation links to a query, you can click on that link to run the related query and see results in [Explore Split View]({{< relref "../../explore#split-and-compare" >}}). +If a correlation links to an external URL, you can click on the link to open the URL in a new tab in your browser. Explore visualizations that currently support showing links based on correlations: diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index b0e4cd732cd..33d5b94b1df 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -86,12 +86,26 @@ Multiple alert instances can be created as a result of one alert rule (also know For Grafana Cloud Free Forever, you can create up to 100 free Grafana-managed alert rules with each alert rule having a maximum of 1000 alert instances. {{% /admonition %}} -Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. +Grafana-managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. -If you delete an alerting resource created in the UI, you can no longer retrieve it. +If you delete an alert resource created in the UI, you can no longer retrieve it. To make a backup of your configuration and to be able to restore deleted alerting resources, create your alerting resources using file provisioning, Terraform, or the Alerting API. -In the following sections, we’ll guide you through the process of creating your Grafana-managed alert rules. +## Before you begin + +You can use default or advanced options for Grafana-managed alert rule creation. The default options streamline rule creation with a cleaner header and a single query and condition. For more complex rules, use advanced options to add multiple queries and expressions. + +Default and advanced options are enabled by default for Grafana Cloud users and this feature is being rolled out progressively. + +For OSS users,enable the `alertingQueryAndExpressionsStepMode` feature toggle. + +{{% admonition type="note" %}} +Once you have created an alert rule using one of the options, the system defaults to this option for the next alert rule you create. + +You can toggle between the two options. However, if you want to switch from advanced options to the default, it may be that your query and expressions cannot be converted. In this case, a warning message checks whether you want to continue to reset to default settings. +{{% /admonition %}} + +## Steps To create a Grafana-managed alert rule, use the in-product alert creation flow and follow these steps. @@ -108,14 +122,26 @@ To get started quickly, refer to our [tutorial on getting started with Grafana a Define a query to get the data you want to measure and a condition that needs to be met before an alert rule fires. +{{< collapse title="Default options" >}} + +1. Add a query. +1. Add an alert condition. + + The **When** input includes the reducer function and the last input is the threshold. + +1. Click **Preview** to verify. + {{< /collapse >}} + +{{< collapse title="Advanced options" >}} + 1. Select a data source. 1. From the **Options** dropdown, specify a [time range](ref:time-units-and-relative-ranges). - **Note:** +{{% admonition type="note" %}} +Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`. - Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`. - - It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`. +It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`. +{{% /admonition %}} 1. Add a query. @@ -138,6 +164,7 @@ Define a query to get the data you want to measure and a condition that needs to You can only add one recovery threshold in a query and it must be the alert condition. 1. Click **Set as alert condition** on the query or expression you want to set as your alert condition. + {{< /collapse >}} ## Set alert evaluation behavior diff --git a/docs/sources/alerting/alerting-rules/templating-labels-annotations.md b/docs/sources/alerting/alerting-rules/templating-labels-annotations.md index 321d8bfd720..d25fa40daec 100644 --- a/docs/sources/alerting/alerting-rules/templating-labels-annotations.md +++ b/docs/sources/alerting/alerting-rules/templating-labels-annotations.md @@ -36,6 +36,64 @@ Each template is evaluated whenever the alert rule is evaluated, and is evaluate Extra whitespace in label templates can break matches with notification policies. {{% /admonition %}} +## Variables + +In Grafana templating, the `$` and `.` symbols are used to reference variables and their properties. You can reference variables directly in your alert rule definitions using the `$` symbol followed by the variable name. Similarly, you can access properties of variables using the dot (`.`) notation within alert rule definitions. + +The following variables are available to you when templating labels and annotations: + +### The labels variable + +The `$labels` variable contains all labels from the query. For example, suppose you have a query that returns CPU usage for all of your servers, and you have an alert rule that fires when any of your servers have exceeded 80% CPU usage for the last 5 minutes. You want to add a summary annotation to the alert that tells you which server is experiencing high CPU usage. With the `$labels` variable you can write a template that prints a human-readable sentence such as: + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes +``` + +> If you are using a classic condition then `$labels` will not contain any labels from the query. Classic conditions discard these labels in order to enforce uni-dimensional behavior (at most one alert per alert rule). If you want to use labels from the query in your template then use the example [here](#print-all-labels-from-a-classic-condition). + +### The value variable + +The `$value` variable is a string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule. It does not contain the results of range queries, as these can return anywhere from 10s to 10,000s of rows or metrics. If it did, for especially large queries a single alert could use 10s of MBs of memory and Grafana would run out of memory very quickly. + +To print the `$value` variable in the summary you would write something like this: + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ $value }} +``` + +And would look something like this: + +``` +CPU usage for instance1 has exceeded 80% for the last 5 minutes: [ var='A' labels={instance=instance1} value=81.234 ] +``` + +Here `var='A'` refers to the instant query with Ref ID A, `labels={instance=instance1}` refers to the labels, and `value=81.234` refers to the average CPU usage over the last 5 minutes. + +If you want to print just some of the string instead of the full string then use the `$values` variable. It contains the same information as `$value`, but in a structured table, and is much easier to use then writing a regular expression to match just the text you want. + +### The values variable + +The `$values` variable is a table containing the labels and floating point values of all instant queries and expressions, indexed by their Ref IDs. + +To print the value of the instant query with Ref ID A: + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "A" }} +``` + +For example, given an alert with the labels `instance=server1` and an instant query with the value `81.2345`, this would print: + +``` +CPU usage for instance1 has exceeded 80% for the last 5 minutes: 81.2345 +``` + +If the query in Ref ID A is a range query rather than an instant query then add a reduce expression with Ref ID B and replace `(index $values "A")` with `(index $values "B")`: + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "B" }} +``` + ## Examples The following examples attempt to show the most common use-cases we have seen for templates. You can use these examples verbatim, or adapt them as necessary for your use case. For more information on how to write text/template refer see [the beginner's guide to alert notification templates in Grafana](https://grafana.com/blog/2023/04/05/grafana-alerting-a-beginners-guide-to-templating-alert-notifications/). @@ -216,62 +274,6 @@ B2: 84.5678 B3: 95.6789 ``` -## Variables - -The following variables are available to you when templating labels and annotations: - -### The labels variable - -The `$labels` variable contains all labels from the query. For example, suppose you have a query that returns CPU usage for all of your servers, and you have an alert rule that fires when any of your servers have exceeded 80% CPU usage for the last 5 minutes. You want to add a summary annotation to the alert that tells you which server is experiencing high CPU usage. With the `$labels` variable you can write a template that prints a human-readable sentence such as: - -``` -CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes -``` - -> If you are using a classic condition then `$labels` will not contain any labels from the query. Classic conditions discard these labels in order to enforce uni-dimensional behavior (at most one alert per alert rule). If you want to use labels from the query in your template then use the example [here](#print-all-labels-from-a-classic-condition). - -### The value variable - -The `$value` variable is a string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule. It does not contain the results of range queries, as these can return anywhere from 10s to 10,000s of rows or metrics. If it did, for especially large queries a single alert could use 10s of MBs of memory and Grafana would run out of memory very quickly. - -To print the `$value` variable in the summary you would write something like this: - -``` -CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ $value }} -``` - -And would look something like this: - -``` -CPU usage for instance1 has exceeded 80% for the last 5 minutes: [ var='A' labels={instance=instance1} value=81.234 ] -``` - -Here `var='A'` refers to the instant query with Ref ID A, `labels={instance=instance1}` refers to the labels, and `value=81.234` refers to the average CPU usage over the last 5 minutes. - -If you want to print just some of the string instead of the full string then use the `$values` variable. It contains the same information as `$value`, but in a structured table, and is much easier to use then writing a regular expression to match just the text you want. - -### The values variable - -The `$values` variable is a table containing the labels and floating point values of all instant queries and expressions, indexed by their Ref IDs. - -To print the value of the instant query with Ref ID A: - -``` -CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "A" }} -``` - -For example, given an alert with the labels `instance=server1` and an instant query with the value `81.2345`, this would print: - -``` -CPU usage for instance1 has exceeded 80% for the last 5 minutes: 81.2345 -``` - -If the query in Ref ID A is a range query rather than an instant query then add a reduce expression with Ref ID B and replace `(index $values "A")` with `(index $values "B")`: - -``` -CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "B" }} -``` - ## Functions The following functions are available to you when templating labels and annotations: diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index 1fa195f52fb..7ab9ffe5385 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -87,6 +87,8 @@ refs: Use contact points to select your preferred communication channel for receiving notifications when your alert rules are firing. You can add, edit, delete, export, and test a contact point. +Testing a contact point is only available for Grafana Alertmanager. + On the **Contact Points** tab, you can: - Search for name and type of contact points and integrations. @@ -144,6 +146,8 @@ Complete the following steps to add templates to your contact point. ## Test a contact point +** For Grafana Alertmanager only.** + Complete the following steps to test a contact point. 1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. @@ -162,6 +166,7 @@ The following table lists the contact point integrations supported by Grafana. | Name | Type | | ---------------------------- | ------------------------- | | Alertmanager | `prometheus-alertmanager` | +| Amazon SNS | `sns` | | Cisco Webex Teams | `webex` | | DingDing | `dingding` | | [Discord](ref:discord) | `discord` | diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md index 6cd38615744..e0209ac8f50 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-amazon-sns.md @@ -18,34 +18,59 @@ weight: 0 # Configure Amazon SNS for Alerting -Use the Grafana Alerting - Amazon SNS integration to send notifications to Amazon SNS when your alerts are firing. +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. ## Before you begin -To configure Amazon SNS to receive alert notifications, complete the following steps. +Before you begin, ensure you have the following: -1. Create a new topic in https://console.aws.amazon.com/sns. -1. Open the topic and create a new subscription. -1. Choose the protocol HTTPS. -1. Copy the URL. +- **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 more information, refer to [Amazon SNS documentation](https://docs.aws.amazon.com/sns/latest/dg/welcome.html). +For an example setup, see [Example Minimal Setup Using Assumed IAM Role]({{< relref "#example-minimal-setup-using-assumed-iam-role" >}}). -## Procedure +## Adding the SNS Contact Point in Grafana -To create your Amazon SNS integration in Grafana Alerting, complete the following steps. +With AWS resources configured, proceed to add SNS as a contact point in Grafana. -1. Navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**. -1. Click **+ Add contact point**. -1. Enter a contact point name. -1. From the Integration list, select **AWS SNS**. -1. Copy in the URL from above into the **The Amazon SNS API URL** field. -1. Click **Test** to check that your integration works. -1. Click **Save contact point**. +- 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. -## Next steps +### 2. Configure SNS Settings -The Amazon SNS contact point is ready to receive alert notifications. +#### 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. + - **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. +- **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. + +Environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. +{{< /admonition >}} + +### 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. @@ -55,3 +80,124 @@ To add this contact point to your alert, complete the following steps. 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. **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)]: + + - On the **Topics** page, choose **"Create topic"**. + - Select **"Standard"** as the type. + - Enter a **Name** for your topic, e.g., `My-Topic`. + - **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)]: + - 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 + +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)]: + + - 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"**. + - 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:::" + } + ] + } + ``` + + - 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:::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)]: + + - In the IAM Console, on the **Users** page, choose **"Create user"**. + - Enter a **User name**, e.g., `grafana-sns-user`. + - Click **"Next"**. + - Select **"Attach policies directly"**. + - Search for the policy you created earlier (`AssumeSNSRolePolicy`) and select it. + - Click **"Next"** , and click **"Create user"**. + +2. **Generate credentials**: + - 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. + +### 5. Add 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: + +- **The Amazon SNS API URL**: `https://sns.us-east-1.amazonaws.com` +- **Signature Version (sigv4)**: + - **Region**: `us-east-1` + - **Access Key**: ``. + - **Secret Key**: `` + - **Role ARN**: `arn:aws:iam:::role/GrafanaSNSRole` +- **SNS topic ARN**: `arn:aws:sns:::My-Topic` + +{{< admonition type="note" >}} +Replace the placeholder values (`https://sns.us-east-1.amazonaws.com`, `us-east-1`, ``, ``, `arn:aws:iam:::role/GrafanaSNSRole`, `arn:aws:sns:::My-Topic`) with your actual AWS credentials and ARNs. +{{< /admonition >}} + +## Additional Resources + +- [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" >}}) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-discord.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-discord.md index 7c5baff4446..399c3b31579 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-discord.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-discord.md @@ -39,6 +39,8 @@ To create your Discord integration in Grafana Alerting, complete the following s 1. In the **Webhook URL** field, paste in your Webhook URL. 1. Click **Test** to check that your integration works. + ** For Grafana Alertmanager only.** + A test alert notification should be sent to the Discord channel that you associated with the Webhook. 1. Click **Save contact point**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-email.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-email.md index 81b6ccac4d8..3b2b1898dc5 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-email.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-email.md @@ -79,6 +79,9 @@ To set up email integration, complete the following steps. E-mail addresses are case sensitive. Ensure that the e-mail address entered is correct. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + 1. Click **Save contact point**. ## Next steps diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-google-chat.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-google-chat.md index e20a31606e9..b0440d06ff2 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-google-chat.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-google-chat.md @@ -39,6 +39,8 @@ To create your Google Chat integration in Grafana Alerting, complete the followi 1. In the **URL** field, paste in your Webhook URL. 1. Click **Test** to check that your integration works. + ** For Grafana Alertmanager only.** + A test alert notification should be sent to the Google Chat space that you associated with the Webhook. 1. Click **Save contact point**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md index 0ded51cd145..0f9567a0a21 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-mqtt.md @@ -14,7 +14,7 @@ labels: - oss menuTitle: MQTT notifier title: Configure the MQTT notifier for Alerting -weight: 80 +weight: 0 --- # Configure the MQTT notifier for Alerting @@ -33,7 +33,11 @@ To configure the MQTT integration for Alerting, complete the following steps. 1. Enter the MQTT topic name in the **Topic** field. 1. In **Optional MQTT settings**, specify additional settings for the MQTT integration if needed. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + A test alert notification should be sent to the MQTT broker. + 1. Click **Save** contact point. The integration sends data in JSON format by default. You can change that using **Message format** field in the **Optional MQTT settings** section. There are two supported formats: diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-opsgenie.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-opsgenie.md index 0ffa3dcf20e..e1f455af2dc 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-opsgenie.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-opsgenie.md @@ -44,6 +44,8 @@ To create your Opsgenie integration in Grafana Alerting, complete the following 1. In the **Alert API URL**, enter `https://api.opsgenie.com/v2/alerts`. 1. Click **Test** to check that your integration works. + ** For Grafana Alertmanager only.** + A test alert notification is sent to the Alerts page in Opsgenie. 1. Click **Save contact point**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md index 6777771bf9a..359cebc1bd3 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-slack.md @@ -78,6 +78,9 @@ To create your Slack integration in Grafana Alerting, complete the following ste - In the **Token** field, copy in the Bot User OAuth Token that starts with “xoxb-”. 1. If you are using a Webhook URL, in the **Webhook** field, copy in your Slack app Webhook URL. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + 1. Click **Save contact point**. ## Next steps diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-teams.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-teams.md index 1141af8db6c..4f720bb616d 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-teams.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-teams.md @@ -43,7 +43,11 @@ To create your MS Teams integration in Grafana Alerting, complete the following 1. From the Integration list, select **Microsoft Teams**. 1. In the **URL** field, copy in your Webhook URL. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + A test alert notification should be sent to the MS Team channel. + 1. Click **Save** contact point. ## Next steps diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md index dd5ca20d7bb..1c86f94d0cc 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/configure-telegram.md @@ -64,6 +64,9 @@ To create your Telegram integration in Grafana Alerting, complete the following 1. In the **BOT API Token** field, copy in the bot API token. 1. In the **Chat ID** field, copy in the chat ID. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + 1. Click **Save contact point**. ## Next steps diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md index d6202d7adc4..ebfdec1f364 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/pager-duty.md @@ -57,6 +57,8 @@ To create your PagerDuty integration in Grafana Alerting, complete the following 1. In the **Integration Key** field, copy in your integration key. 1. Click **Test** to check that your integration works. + ** For Grafana Alertmanager only.** + An incident should display in the Service’s Activity tab in PagerDuty. 1. Click **Save contact point**. diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md index 556d66755ce..17468cac706 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier.md @@ -152,6 +152,9 @@ To create your Webhook integration in Grafana Alerting, complete the following s 1. From the Integration list, select **Webhook**. 1. In the **URL** field, copy in your Webhook URL. 1. Click **Test** to check that your integration works. + + ** For Grafana Alertmanager only.** + 1. Click **Save contact point**. ## Next steps diff --git a/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md b/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md index ea83debb9dc..5de512dd012 100644 --- a/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md +++ b/docs/sources/alerting/fundamentals/alert-rule-evaluation/_index.md @@ -53,19 +53,6 @@ The pending period specifies how long the condition must be met before firing, e You can also set the pending period to zero to skip it and have the alert fire immediately once the condition is met. -## Condition operator - -There are several condition operators available. - -- **and**: Two conditions before and after must be true for the overall condition to be true. -- **or**: If one of conditions before and after are true, the overall condition is true. -- **logic-or**: If the condition before logic-or is true, the overall condition is immediately true, without evaluating subsequent conditions. - -Here are some examples of operators. - -- `TRUE and TRUE or FALSE and FALSE` evaluate to `FALSE`, because last two conditions return `FALSE`. -- `TRUE and TRUE logic-or FALSE and FALSE` evaluate to `TRUE`, because the preceding condition returns `TRUE`. - ## Evaluation example Keep in mind: diff --git a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md index 9e6ece241e1..a3caf9f1f07 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md +++ b/docs/sources/alerting/fundamentals/alert-rules/queries-conditions.md @@ -111,6 +111,12 @@ Classic conditions exist mainly for compatibility reasons and should be avoided Classic condition checks if any time series data matches the alert condition. It always produce one alert instance only, no matter how many time series meet the condition. +| Condition operators | How it works | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| and | Two conditions before and after must be true for the overall condition to be true. | +| or | If one of conditions before and after are true, the overall condition is true. | +| logic-or | If the condition before `logic-or` is true, the overall condition is immediately true, without evaluating subsequent conditions. For instance, `TRUE and TRUE logic-or FALSE and FALSE` evaluate to `TRUE`, because the preceding condition returns `TRUE`. | + ## Aggregations Grafana Alerting provides the following aggregation functions to enable you to further refine your query. diff --git a/docs/sources/alerting/fundamentals/notifications/templates.md b/docs/sources/alerting/fundamentals/notifications/templates.md deleted file mode 100644 index d0ac6b2775b..00000000000 --- a/docs/sources/alerting/fundamentals/notifications/templates.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -aliases: - - ../../contact-points/message-templating/ # /docs/grafana//alerting/contact-points/message-templating/ - - ../../alert-rules/message-templating/ # /docs/grafana//alerting/alert-rules/message-templating/ - - ../../unified-alerting/message-templating/ # /docs/grafana//alerting/unified-alerting/message-templating/ -canonical: https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/templates/ -description: Learn about templates -keywords: - - grafana - - alerting - - guide - - contact point - - templating -labels: - products: - - cloud - - enterprise - - oss -title: Templates -weight: 115 -refs: - variables-label-annotation: - - pattern: /docs/grafana/ - destination: /docs/grafana//alerting/alerting-rules/templating-labels-annotations/ - - pattern: /docs/grafana-cloud/ - destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templating-labels-annotations/ ---- - -# Templates - -Use templating to customize, format, and reuse alert notification messages. Create more flexible and informative alert notification messages by incorporating dynamic content, such as metric values, labels, and other contextual information. - -In Grafana, there are two ways to template your alert notification messages: - -1. Labels and annotations - - - Template labels and annotations in alert rules. - - Labels and annotations contain information about an alert. - - Labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert. - -2. Notification templates - - - Template notifications in contact points. - - Add notification templates to contact points for reuse and consistent messaging in your notifications. - - Use notification templates to change the title, message, and format of the message in your notifications. - -This diagram illustrates the entire process of templating, from the creation of labels and annotations in alert rules or notification templates in contact points, to what they look like when exported and applied in your alert notification messages. - -{{< figure src="/media/docs/alerting/grafana-templating-diagram-2.jpg" max-width="1200px" caption="How Templating works" >}} - -In this diagram: - -- **Monitored Application**: A web server, database, or any other service generating metrics. For example, it could be an NGINX server providing metrics about request rates, response times, and so on. -- **Prometheus**: Prometheus collects metrics from the monitored application. For example, it might scrape metrics from the NGINX server, including labels like instance (the server hostname) and job (the service name). -- **Grafana**: Grafana queries Prometheus to retrieve metrics data. For example, you might create an alert rule to monitor NGINX request rates over time, and template labels or annotations based on the instance label. -- **Alertmanager**: Part of the Prometheus ecosystem, Alertmanager handles alert notifications. For example, if the request rate exceeds a certain threshold on a particular NGINX server, Alertmanager can send an alert notification to, for example, Slack or email, including the server name and the exceeded threshold (the instance label will be interpolated, and the actual server name will appear in the alert notification). -- **Alert notification**: When an alert rule condition is met, Alertmanager sends a notification to various channels such as Slack, Grafana OnCall, etc. These notifications can include information from the labels associated with the alerting rule. For example, if an alert triggers due to high CPU usage on a specific server, the notification message can include details like server name (instance label), disk usage percentage, and the threshold that was exceeded. - -## Labels and annotations - -Labels and annotations contain information about an alert. Labels are used to differentiate an alert from all other alerts, while annotations are used to add additional information to an existing alert. - -### Template labels - -Label templates are applied in the alert rule itself (i.e. in the Configure labels and notifications section of an alert). - -{{}} -Think about templating labels when you need to improve or change how alerts are uniquely identified. This is especially helpful if the labels you get from your query aren't detailed enough. Keep in mind that it's better to keep long sentences for summaries and descriptions. Also, avoid using the query's value in labels because it may result in the creation of many alerts when you actually only need one. -{{}} - -Templating can be applied by using variables and functions. These variables can represent dynamic values retrieved from your data queries. - -{{}} -In Grafana templating, the $ and . symbols are used to reference variables and their properties. You can reference variables directly in your alert rule definitions using the $ symbol followed by the variable name. Similarly, you can access properties of variables using the dot (.) notation within alert rule definitions. -{{}} - -Here are some commonly used built-in [variables](ref:variables-label-annotation) to interact with the name and value of labels in Grafana alerting: - -- The `$labels` variable, which contains all labels from the query. - - For example, let's say you have an alert rule that triggers when the CPU usage exceeds a certain threshold. You want to create annotations that provide additional context when this alert is triggered, such as including the specific server that experienced the high CPU usage. - - The host {{ index $labels "instance" }} has exceeded 80% CPU usage for the last 5 minutes - - The outcome of this template would print: - - The host instance 1 has exceeded 80% CPU usage for the last 5 minutes - -- The `$value` variable, which is a string containing the labels and values of all instant queries; threshold, reduce and math expressions, and classic conditions in the alert rule. - - In the context of the previous example, $value variable would write something like this: - - CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ $value }} - - The outcome of this template would print: - - CPU usage for instance1 has exceeded 80% for the last 5 minutes: [ var='A' labels={instance=instance1} value=81.234 ] - -- The `$values` variable is a table containing the labels and floating point values of all instant queries and expressions, indexed by their Ref IDs (i.e. the id that identifies the query or expression. By default the Red ID of the query is “A”). - - Given an alert with the labels instance=server1 and an instant query with the value 81.2345, would write like this: - - CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes: {{ index $values "A" }} - - And it would print: - - CPU usage for instance1 has exceeded 80% for the last 5 minutes: 81.2345 - -{{% admonition type="caution" %}} -Extra whitespace in label templates can break matches with notification policies. -{{% /admonition %}} - -### Template annotations - -Both labels and annotations have the same structure: a set of named values; however their intended uses are different. The purpose of annotations is to add additional information to existing alerts. - -There are a number of suggested annotations in Grafana such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`. Like labels, annotations must have a name, and their value can contain a combination of text and template code that is evaluated when an alert is fired. - -Here is an example of templating an annotation in the context of an alert rule. The text/template is added into the Add annotations section. - - CPU usage for {{ index $labels "instance" }} has exceeded 80% for the last 5 minutes - -The outcome of this template would print - - CPU usage for Instance 1 has exceeded 80% for the last 5 minutes - -### Template notifications - -Notification templates represent the alternative approach to templating designed for reusing templates. Notifications are messages to inform users about events or conditions triggered by alerts. You can create reusable notification templates to customize the content and format of alert notifications. Variables, labels, or other context-specific details can be added to the templates to dynamically insert information like metric values. - -Here is an example of a notification template: - -```go -{{ define "alerts.message" -}} -{{ if .Alerts.Firing -}} -{{ len .Alerts.Firing }} firing alert(s) -{{ template "alerts.summarize" .Alerts.Firing }} -{{- end }} -{{- if .Alerts.Resolved -}} -{{ len .Alerts.Resolved }} resolved alert(s) -{{ template "alerts.summarize" .Alerts.Resolved }} -{{- end }} -{{- end }} - -{{ define "alerts.summarize" -}} -{{ range . -}} -- {{ index .Annotations "summary" }} -{{ end }} -{{ end }} -``` - -This is the message you would receive in your contact point: - - 1 firing alert(s) - - The database server db1 has exceeded 75% of available disk space. Disk space used is 76%, please resize the disk size within the next 24 hours - - 1 resolved alert(s) - - The web server web1 has been responding to 5% of HTTP requests with 5xx errors for the last 5 minutes - -Once the template is created, you need to make reference to it in your **Contact point** (in the Optional `[contact point]` settings) . - -{{}} -It's not recommended to include individual alert information within notification templates. Instead, it's more effective to incorporate such details within the rule using labels and annotations. -{{}} diff --git a/docs/sources/alerting/fundamentals/templates.md b/docs/sources/alerting/fundamentals/templates.md new file mode 100644 index 00000000000..42b0900ff4c --- /dev/null +++ b/docs/sources/alerting/fundamentals/templates.md @@ -0,0 +1,187 @@ +--- +aliases: + - ../fundamentals/notifications/templates/ # /docs/grafana//alerting/fundamentals/notifications/templates/ + - ../contact-points/message-templating/ # /docs/grafana//alerting/contact-points/message-templating/ + - ../alert-rules/message-templating/ # /docs/grafana//alerting/alert-rules/message-templating/ + - ../unified-alerting/message-templating/ # /docs/grafana//alerting/unified-alerting/message-templating/ +canonical: https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/templates/ +description: Use templating to customize, format, and reuse alert notification messages. Create more flexible and informative alert notification messages by incorporating dynamic content, such as metric values, labels, and other contextual information. +keywords: + - grafana + - alerting + - guide + - contact point + - templating +labels: + products: + - cloud + - enterprise + - oss +title: Templates +meta_image: /media/docs/alerting/how-notification-templates-works.png +weight: 115 +refs: + labels: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/alert-rules/annotation-label/#labels + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#labels + annotations: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/fundamentals/alert-rules/annotation-label/#annotations + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/alert-rules/annotation-label/#annotations + templating-labels-annotations: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/alerting-rules/templating-labels-annotations/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules/templating-labels-annotations/ + notification-message-reference: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/template-notifications/reference/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/reference/ + notification-messages: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/template-notifications/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/ + create-notification-templates: + - pattern: /docs/grafana/ + destination: /docs/grafana//alerting/configure-notifications/template-notifications/create-notification-templates/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/template-notifications/create-notification-templates/ +--- + +# Templates + +Use templating to customize, format, and reuse alert notification messages. Create more flexible and informative alert notification messages by incorporating dynamic content, such as metric values, labels, and other contextual information. + +In Grafana, you have various options to template your alert notification messages: + +1. [Alert rule annotations](#template-annotations) + + - Annotations add extra information, like `summary` and `description`, to alert instances for notification messages. + - Template annotations to display query values that are meaningful to the alert, for example, the server name or the threshold query value. + +1. [Alert rule labels](#template-labels) + + - Labels are used to differentiate an alert instance from all other alert instances. + - Template labels to add an additional label based on a query value, or when the labels from the query are incomplete or not descriptive enough. + +1. [Notification templates](#template-notifications) + - Notification templates are used by contact points for consistent messaging in notification titles and descriptions. + - Template notifications when you want to customize the appearance and information of your notifications. + - Avoid using notification templates to add extra information to alert instances—use annotations instead. + +This diagram illustrates the entire templating process, from querying labels and templating the alert summary and notification to the final alert notification message. + +{{< figure src="/media/docs/alerting/how-notification-templates-works.png" max-width="1200px" caption="How templating works" >}} + +In this diagram: + +1. The alert rule query returns `12345`, along with the values of the `instance` and `job` labels. +1. This query result breaches the alert rule condition, firing the alert instance. +1. The alert instance generates an annotation summary, defined by the template used in the alert rule summary. In this case, it displays the value of the `instance` label: `server1`. +1. The Alertmanager receives the firing alert instance, including the final annotation summary, and determines the contact point that will process the alert. +1. The Alertmanager uses the contact point's notification template to format the message, then sends the notification to the configured destination(s)—an email address. + +## Template annotations + +[Annotations](ref:annotations) can be defined in the alert rule to add extra information to alert instances. + +When creating an alert rule, Grafana suggests several optional annotations, such as `description`, `summary`, `runbook_url`, `dashboardUId` and `panelId`, which help identify and respond to alerts. You can also create custom annotations. + +Annotations are key-value pairs, and their values can contain a combination of text and template code that is evaluated when the alert fires. + +Annotations can contain plain text, but you should template annotations if you need to display query values that are relevant to the alert, for example: + +- Show the query value that triggers the alert. +- Include labels returned by the query that identify the alert. +- Format the annotation message depending on a query value. + +Here’s an example of templating an annotation, which explains where and why the alert was triggered. In this case, the alert triggers when CPU usage exceeds a threshold, and the `summary` annotation provides the relevant details. + +``` +CPU usage for {{ index $labels "instance" }} has exceeded 80% ({{ index $values "A" }}) for the last 5 minutes. +``` + +The outcome of this template would be: + +``` +CPU usage for Instance 1 has exceeded 80% (81.2345) for the last 5 minutes. +``` + +Implement annotations that provide meaningful information to respond to your alerts. Annotations are displayed in the Grafana alert detail view and are included by default in notifications. + +For more details on how to template annotations, refer to [Template annotations and labels](ref:templating-labels-annotations). + +## Template labels + +[Labels](ref:labels) are used to differentiate one alert instance from all other alert instances, as the set of labels uniquely identifies an alert instance. Notification policies and silences use labels to handle alert instances. + +Template labels when you need to improve or change how alerts are uniquely identified. This is helpful if the labels you get from your query aren't detailed enough. + +Here’s an example of templating a `severity` label based on the query value: + +``` +{{ if (gt $values.A.Value 90.0) -}} +critical +{{ else if (gt $values.A.Value 80.0) -}} +high +{{ else if (gt $values.A.Value 60.0) -}} +medium +{{ else -}} +low +{{- end }} +``` + +Avoid using query values in labels, as this may result in the creation of numerous alerts when only one is needed. Use annotation to inform about the query value instead. + +For more details on how to template labels, refer to [Template annotations and labels](ref:templating-labels-annotations). + +## Template notifications + +[Notification templates](ref:notification-messages) allow you to customize the content of your notifications, such as the subject of an email or the body of a Slack message. + +Notification templates differ from templating annotations and labels in the following ways: + +- Notification templates are assigned to the **Contact point**, rather than the alert rule. +- If not specified, the contact point uses a default template that includes relevant alert information. +- You can create reusable notification templates and reference them in other templates. +- The same template can be shared across multiple contact points, making it easier to maintain and ensuring consistency. +- While both annotation/label templates and notification templates use the same templating language, the available variables and functions differ. For more details, refer to the [notification template reference](ref:notification-message-reference) and [annotation/label template reference](ref:templating-labels-annotations). +- Notification templates should not be used to add additional information to individual alerts—use annotations for that purpose. + +Here is an example of a notification template that summarizes all firing and resolved alerts in a notification group: + +``` +{{ define "alerts.message" -}} + {{ if .Alerts.Firing -}} + {{ len .Alerts.Firing }} firing alert(s) + {{ template "alerts.summarize" .Alerts.Firing }} + {{- end }} + {{- if .Alerts.Resolved -}} + {{ len .Alerts.Resolved }} resolved alert(s) + {{ template "alerts.summarize" .Alerts.Resolved }} + {{- end }} +{{- end }} + +{{ define "alerts.summarize" -}} + {{ range . -}} + - {{ index .Annotations "summary" }} + {{ end }} +{{ end }} +``` + +The notification message to the contact point would look like this: + +``` +1 firing alert(s) +- The database server db1 has exceeded 75% of available disk space. Disk space used is 76%, please resize the disk size within the next 24 hours. + +1 resolved alert(s) +- The web server web1 has been responding to 5% of HTTP requests with 5xx errors for the last 5 minutes. +``` + +For instructions on creating and using notification templates, refer to [Create notification templates.](ref:create-notification-templates) diff --git a/docs/sources/dashboards/create-reports/index.md b/docs/sources/dashboards/create-reports/index.md index 5ce55c6b50e..29beabb4794 100644 --- a/docs/sources/dashboards/create-reports/index.md +++ b/docs/sources/dashboards/create-reports/index.md @@ -1,10 +1,10 @@ --- aliases: - - ../administration/reports/ - - ../enterprise/export-pdf/ - - ../enterprise/reporting/ - - ../panels/create-reports/ - - reporting/ + - ../administration/reports/ # /docs/grafana/latest/administration/reports/ + - ../enterprise/export-pdf/ # /docs/grafana/latest/enterprise/export-pdf/ + - ../enterprise/reporting/ # /docs/grafana/latest/enterprise/reporting/ + - ../panels/create-reports/ # /docs/grafana/latest/panels/create-reports/ + - reporting/ # /docs/grafana/latest/dashboards/reporting/ keywords: - grafana - reporting diff --git a/docs/sources/dashboards/share-dashboards-panels/_index.md b/docs/sources/dashboards/share-dashboards-panels/_index.md index 26b269b1231..68c8715c570 100644 --- a/docs/sources/dashboards/share-dashboards-panels/_index.md +++ b/docs/sources/dashboards/share-dashboards-panels/_index.md @@ -1,18 +1,13 @@ --- aliases: - - ../administration/reports/ - - ../enterprise/export-pdf/ - - ../enterprise/reporting/ - - ../reference/share_dashboard/ - - ../reference/share_panel/ - - ../share-dashboards-panels/ - - ../sharing/ - - ../sharing/playlists/ - - ../sharing/share-dashboard/ - - ../sharing/share-panel/ - - ./ - - reporting/ - - share-dashboard/ + - ../reference/share_dashboard/ # /docs/grafana/latest/reference/share_dashboard/ + - ../reference/share_panel/ # /docs/grafana/latest/reference/share_panel/ + - ../share-dashboards-panels/ # /docs/grafana/latest/share-dashboards-panels/ + - ../sharing/ # /docs/grafana/latest/sharing/ + - ../sharing/playlists/ # /docs/grafana/latest/sharing/playlists/ + - ../sharing/share-dashboard/ # /docs/grafana/latest/sharing/share-dashboard/ + - ../sharing/share-panel/ # /docs/grafana/latest/sharing/share-panel/ + - share-dashboard/ # /docs/grafana/latest/dashboards/share-dashboard/ keywords: - grafana - dashboard diff --git a/docs/sources/datasources/loki/query-editor/index.md b/docs/sources/datasources/loki/query-editor/index.md index 3ff47a25215..37750aadd10 100644 --- a/docs/sources/datasources/loki/query-editor/index.md +++ b/docs/sources/datasources/loki/query-editor/index.md @@ -171,6 +171,8 @@ The following options are the same for both **Builder** and **Code** mode: - **Line limit** -Defines the upper limit for the number of log lines returned by a query. The default is `1000` +- **Direction** - Determines the search order. **Backward** is a backward search starting at the end of the time range. **Forward** is a forward search starting at the beginning of the time range. The default is **Backward** + - **Step** Sets the step parameter of Loki metrics queries. The default value equals to the value of `$__interval` variable, which is calculated using the time range and the width of the graph (the number of pixels). - **Resolution** Deprecated. Sets the step parameter of Loki metrics range queries. With a resolution of `1/1`, each pixel corresponds to one data point. `1/2` retrieves one data point for every other pixel, `1/10` retrieves one data point per 10 pixels, and so on. Lower resolutions perform better. diff --git a/docs/sources/datasources/mysql/_index.md b/docs/sources/datasources/mysql/_index.md index 3125a0b2452..783d7ee1a2f 100644 --- a/docs/sources/datasources/mysql/_index.md +++ b/docs/sources/datasources/mysql/_index.md @@ -73,7 +73,7 @@ For instructions on how to add a data source to Grafana, refer to the [administr Only users with the organization administrator role can add data sources. Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system. -{{< docs/play title="MySQL: Cities of the World Sample Data Set" url="https://play.grafana.org/d/8JOvPQr7k/" >}} +{{< docs/play title="MySQL Overview" url="https://play.grafana.org/d/edyh1ib7db6rkb/mysql-overview" >}} ## Configure the data source diff --git a/docs/sources/datasources/postgres/_index.md b/docs/sources/datasources/postgres/_index.md index 86f769d5a77..4b1ab7898c4 100644 --- a/docs/sources/datasources/postgres/_index.md +++ b/docs/sources/datasources/postgres/_index.md @@ -66,6 +66,8 @@ For instructions on how to add a data source to Grafana, refer to the [administr Only users with the organization administrator role can add data sources. Administrators can also [configure the data source via YAML](#provision-the-data-source) with Grafana's provisioning system. +{{< docs/play title="PostgreSQL Overview" url="https://play.grafana.org/d/ddvpgdhiwjvuod/postgresql-overview" >}} + ## PostgreSQL settings To configure basic settings for the data source, complete the following steps: diff --git a/docs/sources/datasources/tempo/query-editor/_index.md b/docs/sources/datasources/tempo/query-editor/_index.md index 28dd3659f63..d2f37e700f7 100644 --- a/docs/sources/datasources/tempo/query-editor/_index.md +++ b/docs/sources/datasources/tempo/query-editor/_index.md @@ -46,6 +46,11 @@ refs: destination: /docs/grafana//explore/explore-inspector/ - pattern: /docs/grafana-cloud/ destination: /docs/grafana//explore/explore-inspector/ + explore-traces-app: + - pattern: /docs/grafana/ + destination: /docs/grafana//explore/simplified-exploration/traces/ + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana-cloud/visualizations/simplified-exploration/traces/ --- # Query tracing data @@ -55,6 +60,10 @@ The queries use [TraceQL](/docs/tempo/latest/traceql), the query language design For general documentation on querying data sources in Grafana, refer to [Query and transform data](ref:query-transform-data). +{{< admonition type="tip" >}} +Don't know TraceQL? Try [Explore Traces](ref:explore-traces-app), an intuitive, queryless app that lets you explore your tracing data using RED metrics. +{{< /admonition >}} + ## Before you begin You can compose TraceQL queries in Grafana and Grafana Cloud using **Explore** and a Tempo data source. diff --git a/docs/sources/explore/simplified-exploration/_index.md b/docs/sources/explore/simplified-exploration/_index.md index b3b454a2c5c..2162eb693ce 100644 --- a/docs/sources/explore/simplified-exploration/_index.md +++ b/docs/sources/explore/simplified-exploration/_index.md @@ -40,4 +40,8 @@ The Grafana Explore apps are designed for effortless data exploration through in Easily explore telemetry signals with these specialized tools, tailored specifically for the Grafana databases to provide quick and accurate insights. +To learn more, read [A queryless experience for exploring metrics, logs, traces, and profiles: Introducing the Explore apps suite for Grafana](https://grafana.com/blog/2024/09/24/queryless-metrics-logs-traces-profiles/). + +{{< youtube id="MSHeWWsHaIA" >}} + {{< card-grid key="cards" type="simple" >}} diff --git a/docs/sources/panels-visualizations/visualizations/geomap/index.md b/docs/sources/panels-visualizations/visualizations/geomap/index.md index d1578cc5063..b42d1f78a6c 100644 --- a/docs/sources/panels-visualizations/visualizations/geomap/index.md +++ b/docs/sources/panels-visualizations/visualizations/geomap/index.md @@ -51,7 +51,7 @@ refs: Geomaps allow you to view and customize the world map using geospatial data. It's the ideal visualization if you have data that includes location information and you want to see it displayed in a map. -You can configure and overlay [map layers](#types), like heatmaps and networks, and blend included basemaps or your own custom maps. This helps you to easily focus on the important location-based characteristics of the data. +You can configure and overlay [map layers](#layer-type), like heatmaps and networks, and blend included basemaps or your own custom maps. This helps you to easily focus on the important location-based characteristics of the data. {{< figure src="/static/img/docs/geomap-panel/geomap-example-8-1-0.png" max-width="1200px" alt="Geomap visualization" >}} @@ -141,42 +141,42 @@ The other location types— airport codes or US state codes—aren't aut If you want to use other codes or give the field a custom name, you can follow the steps in the [Location mode](#location-mode) section. -## Panel options +## Configuration options + +### Panel options {{< docs/shared lookup="visualizations/panel-options.md" source="grafana" version="" >}} -## Map View +### Map view options The map view controls the initial view of the map when the dashboard loads. -### Initial View +#### Initial View The initial view configures how the geomap renders when the panel is first loaded. -- **View** sets the center for the map when the panel first loads. - - **Fit to data** fits the map view based on the data extents of Map layers and updates when data changes. - - **Data** option allows selection of extent based on data from "All layers", a single "Layer", or the "Last value" from a selected layer. - - **Layer** can be selected if fitting data from a single "Layer" or the "Last value" of a layer. - - **Padding** sets padding in relative percent beyond data extent (not available when looking at "Last value" only). - - **Max Zoom** sets the maximum zoom level when fitting data. - - **Coordinates** sets the map view based on: - - **Latitude** - - **Longitude** - - Default Views are also available including: - - **(0°, 0°)** - - **North America** - - **South America** - - **Europe** - - **Africa** - - **West Asia** - - **South Asia** - - **South-East Asia** - - **East Asia** - - **Australia** - - **Oceania** -- **Zoom** sets the initial zoom level. +- **View** - Sets the center for the map when the panel first loads. Refer to the table following this list for view selections. +- **Zoom** - Sets the initial zoom level. +- **Use current map settings** - Use the settings of the current map to set the center. -### Share view + +| View selection | Description | +|---|---| +| Fit to data | fits the map view based on the data extents of Map layers and updates when data changes.
  • **Data** - option allows selection of extent based on data from "All layers", a single "Layer", or the "Last value" from a selected layer.
  • **Layer** - can be selected if fitting data from a single "Layer" or the "Last value" of a layer.
  • **Padding** - sets padding in relative percent beyond data extent (not available when looking at "Last value" only).
  • **Max zoom** - sets the maximum zoom level when fitting data.
  • | +| (0°, 0°) | | +| Coordinates | sets the map view based on: **Latitude** and **Longitude**. | + + +Default Views are also available including: + + +| | | | | | +| ------------- | ------------- | ------ | ------ | --------- | +| North America | South America | Europe | Africa | West Asia | +| South Asia | South-East Asia | East Asia | Australia | Oceania | + + +#### Share view The **Share view** option allows you to link the movement and zoom actions of multiple map visualizations within the same dashboard. The map visualizations that have this option enabled act in tandem when one of them is moved or zoomed, leaving the other ones independent. @@ -184,11 +184,28 @@ The **Share view** option allows you to link the movement and zoom actions of mu You might need to reload the dashboard for this feature to work. {{< /admonition >}} -## Map layers +### Map layers options Geomaps support showing multiple layers. Each layer determines how you visualize geospatial data on top of the base map. -### Types +There are three options that you need to set for all maps: + +- [Layer type](#layer-type) +- [Data](#data) +- [Location mode](#location-mode) + +Other options are dependent on your map layer type and are described within the layer type section. + +The layer controls allow you to create layers, change their name, reorder and delete layers. + +- **Add layer** creates an additional, configurable data layer for the geomap. When you add a layer, you are prompted to select a layer type. You can change the layer type at any point during panel configuration. See the **Layer Types** section above for details on each layer type. +- **Edit layer name (pencil icon)** renames the layer. +- **Trash Bin** deletes the layer. +- **Reorder (six dots/grab handle)** allows you to change the layer order. Data on higher layers will appear above data on lower layers. The visualization will update the layer order as you drag and drop to help simplify choosing a layer order. + +You can add multiple layers of data to a single geomap in order to create rich, detailed visualizations. + +#### Layer type There are seven map layer types to choose from in a geomap. @@ -199,6 +216,10 @@ There are seven map layer types to choose from in a geomap. - [Route (Beta)](#route-layer-beta) render data points as a route. - [Photos (Beta)](#photos-layer-beta) renders a photo at each data point. - [Network (Beta)](#network-layer-beta) visualizes a network graph from the data. +- [Open Street Map](#open-street-map-layer) adds a map from a collaborative free geographic world database. +- [CARTO basemap](#carto-basemap-layer) adds a layer from CARTO Raster basemaps. +- [ArcGIS MapServer](#arcgis-mapserver-layer) adds a layer from an ESRI ArcGIS MapServer. +- [XYZ Tile layer](#xyz-tile-layer) adds a map from a generic tile layer. {{% admonition type="note" %}} Beta is equivalent to the [public preview](/docs/release-life-cycle/) release stage. @@ -209,9 +230,7 @@ There are also two experimental (or alpha) layer types. - **Icon at last point (alpha)** renders an icon at the last data point. - **Dynamic GeoJSON (alpha)** styles a GeoJSON file based on query results. -{{% admonition type="note" %}} -To enable experimental layers: -Set `enable_alpha` to `true` in your configuration file: +To enable experimental layers. Set `enable_alpha` to `true` in your configuration file: ``` [panels] @@ -224,29 +243,11 @@ To enable the experimental layers using Docker, run the following command: docker run -p 3000:3000 -e "GF_PANELS_ENABLE_ALPHA=true" grafana/grafana: ``` -{{% /admonition %}} - -{{% admonition type="note" %}} -[Basemap layer types](#types-1) can also be added as layers. You can specify an opacity. -{{% /admonition %}} - -### Layer Controls - -The layer controls allow you to create layers, change their name, reorder and delete layers. - -- **Add layer** creates an additional, configurable data layer for the geomap. When you add a layer, you are prompted to select a layer type. You can change the layer type at any point during panel configuration. See the **Layer Types** section above for details on each layer type. -- The layer controls allow you to rename, delete, and reorder the layers of the visualization. - - **Edit layer name (pencil icon)** renames the layer. - - **Trash Bin** deletes the layer. - - **Reorder (six dots/grab handle)** allows you to change the layer order. Data on higher layers will appear above data on lower layers. The visualization will update the layer order as you drag and drop to help simplify choosing a layer order. - -You can add multiple layers of data to a single geomap in order to create rich, detailed visualizations. - -### Data +#### Data Geomaps need a source of geographical data gathered from a data source query which can return multiple datasets. By default Grafana picks the first dataset, but this drop-down allows you to pick other datasets if the query returns more than one. -### Location mode +#### Location mode There are four options to map the data returned by the selected query: @@ -259,23 +260,290 @@ There are four options to map the data returned by the selected query: - **Geohash** specifies that your query holds geohash data. You will be prompted to select a string data field for the geohash from your database query. - **Lookup** specifies that your query holds location name data that needs to be mapped to a value. You will be prompted to select the lookup field from your database query and a gazetteer. The gazetteer is the directory that is used to map your queried data to a geographical point. -## Basemap layer +#### Markers layer + +The markers layer allows you to display data points as different marker shapes such as circles, squares, triangles, stars, and more. + +![Markers Layer](/static/img/docs/geomap-panel/geomap-markers-8-1-0.png) + + +| Option | Description | +| ------ | ----------- | +| Data | Configure the data settings for the layer. For more information, refer to [Data](#data). | +| Location | Configure the data settings for the layer. For more information, refer to [Location mode](#location-mode). | +| Size | Configures the size of the markers. The default is `Fixed size`, which makes all marker sizes the same regardless of the data; however, there is also an option to size the markers based on data corresponding to a selected field. `Min` and `Max` marker sizes have to be set such that the markers can scale within this range. | +| Symbol | Allows you to choose the symbol, icon, or graphic to aid in providing additional visual context to your data. Choose from assets that are included with Grafana such as simple symbols or the Unicon library. You can also specify a URL containing an image asset. The image must be a scalable vector graphic (SVG). | +| Symbol Vertical Align | Configures the vertical alignment of the symbol relative to the data point. Note that the symbol's rotation angle is applied first around the data point, then the vertical alignment is applied relative to the rotation of the symbol. | +| Symbol Horizontal Align | Configures the horizontal alignment of the symbol relative to the data point. Note that the symbol's rotation angle is applied first around the data point, then the horizontal alignment is applied relative to the rotation of the symbol. | +| Color | Configures the color of the markers. The default `Fixed color` sets all markers to a specific color. There is also an option to have conditional colors depending on the selected field data point values and the color scheme set in the `Standard options` section. | +| Fill opacity | Configures the transparency of each marker. | +| Rotation angle | Configures the rotation angle of each marker. The default is `Fixed value`, which makes all markers rotate to the same angle regardless of the data; however, there is also an option to set the rotation of the markers based on data corresponding to a selected field. | +| Text label | Configures a text label for each marker. | +| Show legend | Allows you to toggle the legend for the layer. | +| Display tooltip | Allows you to toggle tooltips for the layer. | + + +#### Heatmap layer + +The heatmap layer clusters various data points to visualize locations with different densities. +To add a heatmap layer: + +Click on the drop-down menu under Data Layer and choose `Heatmap`. + +Similar to `Markers`, you are prompted with various options to determine which data points to visualize and how you want to visualize them. + +![Heatmap Layer](/static/img/docs/geomap-panel/geomap-heatmap-8-1-0.png) + + +| Option | Description | +| ------ | ----------- | +| Data | Configure the data settings for the layer. For more information, refer to [Data](#data). | +| Location | Configure the data settings for the layer. For more information, refer to [Location mode](#location-mode). | +| Weight values | Configures the size of the markers. The default is `Fixed size`, which makes all marker sizes the same regardless of the data; however, there is also an option to size the markers based on data corresponding to a selected field. `Min` and `Max` marker sizes have to be set such that the markers can scale within this range. | +| Radius | Configures the size of the heatmap clusters. | +| Blur | Configures the amount of blur on each cluster. | +| Opacity | Configures the opacity of each cluster. | +| Display tooltip | Allows you to toggle tooltips for the layer. | + + +#### GeoJSON layer + +The GeoJSON layer allows you to select and load a static GeoJSON file from the filesystem. + + +| Option | Description | +| ------ | ----------- | +| GeoJSON URL | Provides a choice of GeoJSON files that ship with Grafana. | +| Default Style | Controls which styles to apply when no rules above match.
    • **Color** - configures the color of the default style
    • **Opacity** - configures the default opacity
    | +| Style Rules | Apply styles based on feature properties
    • **Rule** - allows you to select a _feature_, _condition_, and _value_ from the GeoJSON file in order to define a rule. The trash bin icon can be used to delete the current rule.
    • **Color** - configures the color of the style for the current rule
    • **Opacity** - configures the transparency level for the current rule
    • | +| Display tooltip | Allows you to toggle tooltips for the layer. | + + +Styles can be set within the "properties" object of the GeoJSON with support for the following geometries: + +**Polygon, MultiPolygon** + +- **"fill"** - The color of the interior of the polygon(s) +- **"fill-opacity"** - The opacity of the interior of the polygon(s) +- **"stroke-width"** - The width of the line component of the polygon(s) + +**Point, MultiPoint** + +- **"marker-color"** - The color of the point(s) +- **"marker-size"** - The size of the point(s) + +**LineString, MultiLineString** + +- **"stroke"** - The color of the line(s) +- **"stroke-width"** - The width of the line(s) + +#### Night / Day layer + +The Night / Day layer displays night and day regions based on the current time range. + +{{< figure src="/static/img/docs/geomap-panel/geomap-day-night-9-1-0.png" max-width="1200px" alt="Geomap panel Night / Day" >}} + + +| Option | Description | +| ------ | ----------- | +| Data | Configures the data set for the layer. For more information, refer to [Data](#data). | +| Show | Toggles the time source from panel time range. | +| Night region color | Picks the color for the night region. | +| Display sun | Toggles the sun icon. | +| Opacity | Set the opacity from `0` (transparent) to `1` (opaque). | +| Display tooltip | Allows you to toggle tooltips for the layer. | + + +[Extensions for OpenLayers - DayNight](https://viglino.github.io/ol-ext/examples/layer/map.daynight.html) + +#### Route layer (Beta) + +{{% admonition type="caution" %}} +The Route layer is currently in [public preview](/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. +{{% /admonition %}} + +The Route layer renders data points as a route. + +{{< figure src="/media/docs/grafana/geomap-route-layer-basic-9-4-0.png" max-width="1200px" alt="Geomap panel Route" >}} + +The layer can also render a route with arrows. + +{{< figure src="/media/docs/grafana/geomap-route-layer-arrow-size-9-4-0.png" max-width="1200px" alt="Geomap panel Route arrows with size" >}} + + +| Option | Description | +| ------ | ----------- | +| Data | configure the data settings for the layer. For more information, refer to [Data](#data). | +| Location | configure the data settings for the layer. For more information, refer to [Location mode](#location-mode). | +| Size | sets the route thickness. Fixed value by default. When field data is selected you can set the Min and Max range in which field data can scale. | +| Color | sets the route color. Set to `Fixed color` by default. You can also tie the color to field data. | +| Fill opacity | configures the opacity of the route. | +| Text label | configures a text label for each route. | +| Arrow | sets the arrow styling to display along route, in order of data. Choose from: **None**, **Forward**, and **Reverse** | +| Display tooltip | allows you to toggle tooltips for the layer. | + + +[Extensions for OpenLayers - Flow Line Style](http://viglino.github.io/ol-ext/examples/style/map.style.gpxline.html) + +#### Photos layer (Beta) + +{{% admonition type="caution" %}} +The Photos layer is currently in [public preview](/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. +{{% /admonition %}} + +The Photos layer renders a photo at each data point. + +{{< figure src="/static/img/docs/geomap-panel/geomap-photos-9-3-0.png" max-width="1200px" alt="Geomap panel Photos" >}} + + +| Option | Description | +| ------ | ----------- | +| Data | Configure the data settings for the layer. For more information, refer to [Data](#data). | +| Location | Configure the data settings for the layer. For more information, refer to [Location mode](#location-mode). | +| Image Source field | Allows you to select a string field containing image data in either of the following formats:
      • **Image URLs**
      • **Base64 encoded** - Image binary ("data:image/png;base64,...")
      | +| Kind | Sets the frame style around the images. Choose from: **Square**, **Circle**, **Anchored**, and **Folio**. | +| Crop | Toggles whether the images are cropped to fit. | +| Shadow | Toggles a box shadow behind the images. | +| Border | Sets the border size around images. | +| Border color | Sets the border color around images. | +| Radius | Sets the overall size of images in pixels. | +| Display tooltip | Allows you to toggle tooltips for the layer. | + + +[Extensions for OpenLayers - Image Photo Style](http://viglino.github.io/ol-ext/examples/style/map.style.photo.html) + +#### Network layer (Beta) + +{{% admonition type="caution" %}} +The Network layer is currently in [public preview](/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. +{{% /admonition %}} + +The Network layer renders a network graph. This layer supports the same [data format supported by the node graph visualization](ref:data-format) with the addition of [geospatial data](#location-mode) included in the nodes data. The geospatial data is used to locate and render the nodes on the map. + +{{< figure src="/media/docs/grafana/screenshot-grafana-10-1-geomap-network-layer-v2.png" max-width="750px" alt="Geomap network layer" >}} + +You can convert node graph data to a network layer: +{{< video-embed src="/media/docs/grafana/screen-recording-10-1-geomap-network-layer-from-node-graph.mp4" max-width="750px" alt="Node graph to Geomap network layer" >}} + + +| Option | Description | +| ------ | ----------- | +| Data | Configure the data settings for the layer. For more information, refer to [Data](#data). | +| Location | Configure the data settings for the layer. For more information, refer to [Location mode](#location-mode). | +| Arrow | Sets the arrow direction to display for each edge, with forward meaning source to target. Choose from: **None**, **Forward**, **Reverse** and **Both**. | +| Show legend | Allows you to toggle the legend for the layer. **Note:** The legend currently only supports node data. | +| Display tooltip | Allows you to toggle tooltips for the layer. | + + +##### Node styles options + + +| Option | Description | +| ------ | ----------- | +| Size | Configures the size of the nodes. The default is `Fixed size`, which makes all node sizes the same regardless of the data; however, there is also an option to size the nodes based on data corresponding to a selected field. `Min` and `Max` node sizes have to be set such that the nodes can scale within this range. | +| Symbol | Allows you to choose the symbol, icon, or graphic to aid in providing additional visual context to your data. Choose from assets that are included with Grafana such as simple symbols or the Unicon library. You can also specify a URL containing an image asset. The image must be a scalable vector graphic (SVG). | +| Color | Configures the color of the nodes. The default `Fixed color` sets all nodes to a specific color. There is also an option to have conditional colors depending on the selected field data point values and the color scheme set in the `Standard options` section. | +| Fill opacity | Configures the transparency of each node. | +| Rotation angle | Configures the rotation angle of each node. The default is `Fixed value`, which makes all nodes rotate to the same angle regardless of the data; however, there is also an option to set the rotation of the nodes based on data corresponding to a selected field. | +| Text label | Configures a text label for each node. | + + +##### Edge styles options + + +| Option | Description | +| ------ | ----------- | +| Size | Configures the line width of the edges. The default is `Fixed size`, which makes all edge line widths the same regardless of the data; however, there is also an option to size the edges based on data corresponding to a selected field. `Min` and `Max` eges sizes have to be set such that the edges can scale within this range. | +| Color | Configures the color of the edges. The default `Fixed color` sets all edges to a specific color. There is also an option to have conditional colors depending on the selected field data point values and the color scheme set in the `Standard options` section. | +| Fill opacity | Configures the transparency of each edge. | +| Text label | Configures a text label for each edge. | + + +#### Open Street Map layer + +A map from a collaborative free geographic world database. + +{{< figure src="/static/img/docs/geomap-panel/geomap-osm-9-1-0.png" max-width="1200px" alt="Geomap panel Open Street Map" >}} + +- **Opacity** from 0 (transparent) to 1 (opaque) +- **Display tooltip** - allows you to toggle tooltips for the layer. + +[About Open Street Map](https://www.openstreetmap.org/about) + +#### CARTO basemap layer + +A CARTO layer is from CARTO Raster basemaps. + +- **Theme** + - Auto + - Light + {{< figure src="/static/img/docs/geomap-panel/geomap-carto-light-9-1-0.png" max-width="1200px" alt="Geomap panel CARTO light example" >}} + - Dark + {{< figure src="/static/img/docs/geomap-panel/geomap-carto-dark-9-1-0.png" max-width="1200px" alt="Geomap panel CARTO dark example" >}} +- **Show labels** shows the Country details on top of the map. +- **Opacity** from 0 (transparent) to 1 (opaque) +- **Display tooltip** - allows you to toggle tooltips for the layer. + +[About CARTO](https://carto.com/about-us/) + +#### ArcGIS MapServer layer + +An ArcGIS layer is a layer from an ESRI ArcGIS MapServer. + +- **Server Instance** to select the map type. + - World Street Map + {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-wsm-9-1-0.png" max-width="1200px" alt="Geomap panel ArcGIS World Street Map" >}} + - World Imagery + {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-wi-9-1-0.png" max-width="1200px" alt="Geomap panel ArcGIS World Imagery" >}} + - World Physical + {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-wp-9-1-0.png" max-width="1200px" alt="Geomap panel ArcGIS World Physical" >}} + - Topographic + {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-topographic-9-1-0.png" max-width="1200px" alt="Geomap panel ArcGIS Topographic" >}} + - USA Topographic + {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-usa-topographic-9-1-0.png" max-width="1200px" alt="Geomap panel ArcGIS USA Topographic" >}} + - World Ocean + {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-ocean-9-1-0.png" max-width="1200px" alt="Geomap panel ArcGIS World Ocean" >}} + - Custom MapServer (see [XYZ](#xyz-tile-layer) for formatting) + - URL template + - Attribution +- **Opacity** from 0 (transparent) to 1 (opaque) +- **Display tooltip** - allows you to toggle tooltips for the layer. + +##### More Information + +- [ArcGIS Services](https://services.arcgisonline.com/arcgis/rest/services) +- [About ESRI](https://www.esri.com/en-us/about/about-esri/overview) + +#### XYZ Tile layer + +The XYZ Tile layer is a map from a generic tile layer. + +{{< figure src="/static/img/docs/geomap-panel/geomap-xyz-9-1-0.png" max-width="1200px" alt="Geomap panel xyz example" >}} + +- **URL template** - Set a valid tile server url, with {z}/{x}/{y} for example: https://tile.openstreetmap.org/{z}/{x}/{y}.png +- **Attribution** sets the reference string for the layer if displayed in [map controls](#show-attribution) +- **Opacity** from 0 (transparent) to 1 (opaque) + +##### More information + +- [Tiled Web Map Wikipedia](https://en.wikipedia.org/wiki/Tiled_web_map) +- [List of Open Street Map Tile Servers](https://wiki.openstreetmap.org/wiki/Tile_servers) + +### Basemap layer options A basemap layer provides the visual foundation for a mapping application. It typically contains data with global coverage. Several base layer options are available each with specific configuration options to style the base map. -### Types +Basemap layer types can also be added as layers. You can specify an opacity. There are four basemap layer types to choose from in a geomap. - [Open Street Map](#open-street-map-layer) adds a map from a collaborative free geographic world database. -- [CARTO](#carto-layer) adds a layer from CARTO Raster basemaps. -- [ArcGIS](#arcgis-layer) adds a layer from an ESRI ArcGIS MapServer. -- [XYZ](#xyz-tile-layer) adds a map from a generic tile layer. +- [CARTO basemap](#carto-basemap-layer) adds a layer from CARTO Raster basemaps. +- [ArcGIS MapServer](#arcgis-mapserver-layer) adds a layer from an ESRI ArcGIS MapServer. +- [XYZ Tile layer](#xyz-tile-layer) adds a map from a generic tile layer. -### Default - -The default base layer uses the [CARTO](#carto-layer) map. You can define custom default base layers in the `.ini` configuration file. +The default basemap layer uses the CARTO map. You can define custom default base layers in the `.ini` configuration file. ![Basemap layer options](/static/img/docs/geomap-panel/geomap-baselayer-8-1-0.png) @@ -299,6 +567,9 @@ geomap_default_baselayer = `{ - **esri-xyz** loads the ESRI tile server. There are already multiple server instances implemented to show the various map styles: `world-imagery`, `world-physical`, `topo`, `usa-topo`, and `ocean`. The `custom` server option allows you to configure your own ArcGIS map server. Here are some examples: +{{< tabs >}} +{{< tab-content name="World imagery" >}} + ```ini geomap_default_baselayer = `{ "type": "esri-xyz", @@ -308,6 +579,9 @@ geomap_default_baselayer = `{ }` ``` +{{< /tab-content >}} +{{< tab-content name="Custom" >}} + ```ini geomap_default_baselayer = `{ "type": "esri-xyz", @@ -319,6 +593,9 @@ geomap_default_baselayer = `{ }` ``` +{{< /tab-content >}} +{{< /tabs >}} + - **osm-standard** loads the OpenStreetMap tile server. There are no additional configurations needed and the `config` fields can be left blank. Here is an example: ```ini @@ -342,343 +619,57 @@ default_baselayer_config = `{ `enable_custom_baselayers` allows you to enable or disable custom open source base maps that are already implemented. The default is `true`. -## Markers layer - -The markers layer allows you to display data points as different marker shapes such as circles, squares, triangles, stars, and more. - -![Markers Layer](/static/img/docs/geomap-panel/geomap-markers-8-1-0.png) - -{{< figure src="/media/docs/grafana/panels-visualizations/geomap-markers-options-11-1-0.png" max-width="350px" alt="Markers layer options" >}} - -- **Data** and **Location mode** configure the data settings for the layer. For more information, refer to [Data](#data) and [Location mode](#location-mode). -- **Size** configures the size of the markers. The default is `Fixed size`, which makes all marker sizes the same regardless of the data; however, there is also an option to size the markers based on data corresponding to a selected field. `Min` and `Max` marker sizes have to be set such that the markers can scale within this range. -- **Symbol** allows you to choose the symbol, icon, or graphic to aid in providing additional visual context to your data. Choose from assets that are included with Grafana such as simple symbols or the Unicon library. You can also specify a URL containing an image asset. The image must be a scalable vector graphic (SVG). -- **Symbol Vertical Align** configures the vertical alignment of the symbol relative to the data point. Note that the symbol's rotation angle is applied first around the data point, then the vertical alignment is applied relative to the rotation of the symbol. -- **Symbol Horizontal Align** configures the horizontal alignment of the symbol relative to the data point. Note that the symbol's rotation angle is applied first around the data point, then the horizontal alignment is applied relative to the rotation of the symbol. -- **Color** configures the color of the markers. The default `Fixed color` sets all markers to a specific color. There is also an option to have conditional colors depending on the selected field data point values and the color scheme set in the `Standard options` section. -- **Fill opacity** configures the transparency of each marker. -- **Rotation angle** configures the rotation angle of each marker. The default is `Fixed value`, which makes all markers rotate to the same angle regardless of the data; however, there is also an option to set the rotation of the markers based on data corresponding to a selected field. -- **Text label** configures a text label for each marker. -- **Show legend** allows you to toggle the legend for the layer. -- **Display tooltip** allows you to toggle tooltips for the layer. - -## Heatmap layer - -The heatmap layer clusters various data points to visualize locations with different densities. -To add a heatmap layer: - -Click on the drop-down menu under Data Layer and choose `Heatmap`. - -Similar to `Markers`, you are prompted with various options to determine which data points to visualize and how you want to visualize them. - -![Heatmap Layer](/static/img/docs/geomap-panel/geomap-heatmap-8-1-0.png) - -{{< figure src="/media/docs/grafana/panels-visualizations/geomap-heatmap-options-11-1-0.png" max-width="350px" alt="Heatmap layer options" >}} - -- **Data** and **Location mode** configure the data settings for the layer. For more information, refer to [Data](#data) and [Location mode](#location-mode). -- **Weight values** configure the intensity of the heatmap clusters. `Fixed value` keeps a constant weight value throughout all data points. This value should be in the range of 0~1. Similar to Markers, there is an alternate option in the drop-down to automatically scale the weight values depending on data values. -- **Radius** configures the size of the heatmap clusters. -- **Blur** configures the amount of blur on each cluster. -- **Opacity** configures the opacity of each cluster. -- **Display tooltip** allows you to toggle tooltips for the layer. - -## GeoJSON layer - -The GeoJSON layer allows you to select and load a static GeoJSON file from the filesystem. - -- **GeoJSON URL** provides a choice of GeoJSON files that ship with Grafana. -- **Default Style** controls which styles to apply when no rules above match. - - **Color** configures the color of the default style - - **Opacity** configures the default opacity -- **Style Rules** apply styles based on feature properties - - **Rule** allows you to select a _feature_, _condition_, and _value_ from the GeoJSON file in order to define a rule. The trash bin icon can be used to delete the current rule. - - **Color** configures the color of the style for the current rule - - **Opacity** configures the transparency level for the current rule -- **Add style rule** creates additional style rules. -- **Display tooltip** allows you to toggle tooltips for the layer. - -{{% admonition type="note" %}} -Styles can be set within the "properties" object of the GeoJSON with support for the following geometries: - -- Polygon, MultiPolygon - - - **"fill"** - The color of the interior of the polygon(s) - - **"fill-opacity"** - The opacity of the interior of the polygon(s) - - **"stroke-width"** - The width of the line component of the polygon(s) - -- Point, MultiPoint - - - **"marker-color"** - The color of the point(s) - - **"marker-size"** - The size of the point(s) - -- LineString, MultiLineString - - **"stroke"** - The color of the line(s) - - **"stroke-width"** - The width of the line(s) - -{{% /admonition %}} - -## Night / Day layer - -The Night / Day layer displays night and day regions based on the current time range. - -{{< figure src="/static/img/docs/geomap-panel/geomap-day-night-9-1-0.png" max-width="1200px" caption="Geomap panel Night / Day" >}} - -### Options - -- **Data** configures the data set for the layer. For more information, refer to [Data](#data). -- **Show** toggles the time source from panel time range. -- **Night region color** picks the color for the night region. -- **Display sun** toggles the sun icon. -- **Opacity** set the opacity from `0` (transparent) to `1` (opaque). -- **Display tooltip** allows you to toggle tooltips for the layer. - -{{< figure src="/static/img/docs/geomap-panel/geomap-day-night-options-9-1-0.png" max-width="1200px" caption="Geomap panel Night / Day options" >}} - -### More information - -- [**Extensions for OpenLayers - DayNight**](https://viglino.github.io/ol-ext/examples/layer/map.daynight.html) - -## Route layer (Beta) - -{{% admonition type="caution" %}} -The Route layer is currently in [public preview](/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. -{{% /admonition %}} - -The Route layer renders data points as a route. - -{{< figure src="/media/docs/grafana/geomap-route-layer-basic-9-4-0.png" max-width="1200px" caption="Geomap panel Route" >}} - -### Options - -- **Data** and **Location mode** configure the data settings for the layer. For more information, refer to [Data](#data) and [Location mode](#location-mode). -- **Size** sets the route thickness. Fixed value by default. When field data is selected you can set the Min and Max range in which field data can scale. -- **Color** sets the route color. Set to `Fixed color` by default. You can also tie the color to field data. -- **Fill opacity** configures the opacity of the route. -- **Text label** configures a text label for each route. -- **Arrow** sets the arrow styling to display along route, in order of data. - - **None** - - **Forward** - - **Reverse** -- **Display tooltip** allows you to toggle tooltips for the layer. - -{{< figure src="/media/docs/grafana/geomap-route-layer-arrow-size-9-4-0.png" max-width="1200px" caption="Geomap panel Route arrows with size" >}} - -### More information - -- [**Extensions for OpenLayers - Flow Line Style**](http://viglino.github.io/ol-ext/examples/style/map.style.gpxline.html) - -## Photos layer (Beta) - -{{% admonition type="caution" %}} -The Photos layer is currently in [public preview](/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. -{{% /admonition %}} - -The Photos layer renders a photo at each data point. - -{{< figure src="/static/img/docs/geomap-panel/geomap-photos-9-3-0.png" max-width="1200px" caption="Geomap panel Photos" >}} - -### Options - -- **Data** and **Location mode** configure the data settings for the layer. For more information, refer to [Data](#data) and [Location mode](#location-mode). -- **Image Source field** allows you to select a string field containing image data in either of the following formats: - - **Image URLs** - - **Base64 encoded** - Image binary ("data:image/png;base64,...") -- **Kind** sets the frame style around the images. Choose from: - - **Square** - - **Circle** - - **Anchored** - - **Folio** -- **Crop** toggles whether the images are cropped to fit. -- **Shadow** toggles a box shadow behind the images. -- **Border** sets the border size around images. -- **Border color** sets the border color around images. -- **Radius** sets the overall size of images in pixels. -- **Display tooltip** allows you to toggle tooltips for the layer. - -{{< figure src="/static/img/docs/geomap-panel/geomap-photos-options-9-3-0.png" max-width="1200px" caption="Geomap panel Photos options" >}} - -### More information - -- [**Extensions for OpenLayers - Image Photo Style**](http://viglino.github.io/ol-ext/examples/style/map.style.photo.html) - -## Network layer (Beta) - -{{% admonition type="caution" %}} -The Network layer is currently in [public preview](/docs/release-life-cycle/). Grafana Labs offers limited support, and breaking changes might occur prior to the feature being made generally available. -{{% /admonition %}} - -The Network layer renders a network graph. This layer supports the same [data format supported by the node graph visualization](ref:data-format) with the addition of [geospatial data](#location-mode) included in the nodes data. The geospatial data is used to locate and render the nodes on the map. - -{{< figure src="/media/docs/grafana/screenshot-grafana-10-1-geomap-network-layer-v2.png" max-width="750px" caption="Geomap network layer" >}} -{{< video-embed src="/media/docs/grafana/screen-recording-10-1-geomap-network-layer-from-node-graph.mp4" max-width="750px" caption="Node graph to Geomap network layer" >}} - -### Options - -- **Data** and **Location mode** configure the data settings for the layer. For more information, refer to [Data](#data) and [Location mode](#location-mode). -- **Arrow** sets the arrow direction to display for each edge, with forward meaning source to target. Choose from: - - **None** - - **Forward** - - **Reverse** - - **Both** -- **Show legend** allows you to toggle the legend for the layer. **Note:** The legend currently only supports node data. -- **Display tooltip** allows you to toggle tooltips for the layer. - -#### Node styles - -- **Size** configures the size of the nodes. The default is `Fixed size`, which makes all node sizes the same regardless of the data; however, there is also an option to size the nodes based on data corresponding to a selected field. `Min` and `Max` node sizes have to be set such that the nodes can scale within this range. -- **Symbol** allows you to choose the symbol, icon, or graphic to aid in providing additional visual context to your data. Choose from assets that are included with Grafana such as simple symbols or the Unicon library. You can also specify a URL containing an image asset. The image must be a scalable vector graphic (SVG). -- **Color** configures the color of the nodes. The default `Fixed color` sets all nodes to a specific color. There is also an option to have conditional colors depending on the selected field data point values and the color scheme set in the `Standard options` section. -- **Fill opacity** configures the transparency of each node. -- **Rotation angle** configures the rotation angle of each node. The default is `Fixed value`, which makes all nodes rotate to the same angle regardless of the data; however, there is also an option to set the rotation of the nodes based on data corresponding to a selected field. -- **Text label** configures a text label for each node. - -#### Edge styles - -- **Size** configures the line width of the edges. The default is `Fixed size`, which makes all edge line widths the same regardless of the data; however, there is also an option to size the edges based on data corresponding to a selected field. `Min` and `Max` eges sizes have to be set such that the edges can scale within this range. -- **Color** configures the color of the edges. The default `Fixed color` sets all edges to a specific color. There is also an option to have conditional colors depending on the selected field data point values and the color scheme set in the `Standard options` section. -- **Fill opacity** configures the transparency of each edge. -- **Text label** configures a text label for each edge. - -## CARTO layer - -A CARTO layer is from CARTO Raster basemaps. - -### Options - -- **Theme** - - Auto - - Light - {{< figure src="/static/img/docs/geomap-panel/geomap-carto-light-9-1-0.png" max-width="1200px" caption="Geomap panel CARTO light example" >}} - - Dark - {{< figure src="/static/img/docs/geomap-panel/geomap-carto-dark-9-1-0.png" max-width="1200px" caption="Geomap panel CARTO dark example" >}} -- **Show labels** shows the Country details on top of the map. -- **Opacity** from 0 (transparent) to 1 (opaque) - -{{< figure src="/static/img/docs/geomap-panel/geomap-carto-options-9-1-0.png" max-width="1200px" caption="Geomap panel CARTO options" >}} - -### More Information - -- [**About CARTO**](https://carto.com/about-us/) - -## XYZ tile layer - -The XYZ tile layer is a map from a generic tile layer. - -{{< figure src="/static/img/docs/geomap-panel/geomap-xyz-9-1-0.png" max-width="1200px" caption="Geomap panel xyz example" >}} - -### Options - -- **URL template** - - > **Note:** Set a valid tile server url, with {z}/{x}/{y} for example: https://tile.openstreetmap.org/{z}/{x}/{y}.png - -- **Attribution** sets the reference string for the layer if displayed in [map controls](#show-attribution) -- **Opacity** from 0 (transparent) to 1 (opaque) - -{{< figure src="/static/img/docs/geomap-panel/geomap-xyz-options-9-1-0.png" max-width="1200px" caption="Geomap panel xyz options" >}} - -### More information - -- [**Tiled Web Map Wikipedia**](https://en.wikipedia.org/wiki/Tiled_web_map) -- [**List of Open Street Map Tile Servers**](https://wiki.openstreetmap.org/wiki/Tile_servers) - -## Open Street Map layer - -A map from a collaborative free geographic world database. - -{{< figure src="/static/img/docs/geomap-panel/geomap-osm-9-1-0.png" max-width="1200px" caption="Geomap panel Open Street Map" >}} - -### Options - -- **Opacity** from 0 (transparent) to 1 (opaque) - -{{< figure src="/static/img/docs/geomap-panel/geomap-osm-options-9-1-0.png" max-width="1200px" caption="Geomap panel Open Street Map options" >}} - -### More Information - -- [**About Open Street Map**](https://www.openstreetmap.org/about) - -## ArcGIS layer - -An ArcGIS layer is a layer from an ESRI ArcGIS MapServer. - -### Options - -- **Server Instance** to select the map type. - - World Street Map - {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-wsm-9-1-0.png" max-width="1200px" caption="Geomap panel ArcGIS World Street Map" >}} - - World Imagery - {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-wi-9-1-0.png" max-width="1200px" caption="Geomap panel ArcGIS World Imagery" >}} - - World Physical - {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-wp-9-1-0.png" max-width="1200px" caption="Geomap panel ArcGIS World Physical" >}} - - Topographic - {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-topographic-9-1-0.png" max-width="1200px" caption="Geomap panel ArcGIS Topographic" >}} - - USA Topographic - {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-usa-topographic-9-1-0.png" max-width="1200px" caption="Geomap panel ArcGIS USA Topographic" >}} - - World Ocean - {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-ocean-9-1-0.png" max-width="1200px" caption="Geomap panel ArcGIS World Ocean" >}} - - Custom MapServer (see [XYZ](#xyz-tile-layer) for formatting) - - URL template - - Attribution -- **Opacity** from 0 (transparent) to 1 (opaque) - - {{< figure src="/static/img/docs/geomap-panel/geomap-arcgis-options-9-1-0.png" max-width="1200px" caption="Geomap panel ArcGIS options" >}} - -### More Information - -- [**ArcGIS Services**](https://services.arcgisonline.com/arcgis/rest/services) -- [**About ESRI**](https://www.esri.com/en-us/about/about-esri/overview) - -## Map Controls +### Map controls options The map controls section contains various options for map information and tool overlays. -{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-9-1-0.png" max-width="1200px" caption="Geomap panel map controls" >}} -### Zoom - -This section describes each of the zoom controls. + +| Option | Description | +| ------ | ----------- | +| [Show zoom control](#show-zoom-control) | Displays zoom controls in the upper left corner. | +| [Mouse wheel zoom](#mouse-wheel-zoom) | Enables the mouse wheel to be used for zooming in or out. | +| [Show attribution](#show-attribution) | Displays attribution for basemap layers. | +| [Show scale](#show-scale) | Displays scale information in the bottom left corner in meters (m) or kilometers (km). | +| [Show measure tools](#show-measure-tools) | Displays measure tools in the upper right corner. This includes the [Length](#length) and [Area](#area) options. | +| [Show debug](#show-debug) | Displays debug information in the upper right corner. | +| [Tooltip](#tooltip) | Controls display of tooltips. | + #### Show zoom control Displays zoom controls in the upper left corner. This control can be useful when using systems that don't have a mouse. -{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-zoom-9-1-0.png" max-width="1200px" caption="Geomap panel zoom" >}} +{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-zoom-9-1-0.png" max-width="1200px" alt="Geomap panel zoom" >}} #### Mouse wheel zoom Enables the mouse wheel to be used for zooming in or out. -### Show attribution +#### Show attribution Displays attribution for basemap layers. -{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-attribution-9-1-0.png" max-width="1200px" caption="Geomap panel attribution" >}} +{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-attribution-9-1-0.png" max-width="1200px" alt="Geomap panel attribution" >}} -### Show scale +#### Show scale -Displays scale information in the bottom left corner. +Displays scale information in the bottom left corner in meters (m) or kilometers (km). -{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-scale-9-1-0.png" max-width="1200px" caption="Geomap panel scale" >}} +{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-scale-9-1-0.png" max-width="1200px" alt="Geomap panel scale" >}} -{{% admonition type="note" %}} -Currently only displays units in [m]/[km]. -{{% /admonition %}} - -### Show measure tools +#### Show measure tools Displays measure tools in the upper right corner. Measurements appear only when this control is open. -{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-measure-9-1-0.png" max-width="1200px" caption="Geomap panel measure" >}} +{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-measure-9-1-0.png" max-width="1200px" alt="Geomap panel measure" >}} - **Click** to start measuring - **Continue clicking** to continue measurement - **Double-click** to end measurement -{{% admonition type="note" %}} When you change measurement type or units, the previous measurement is removed from the map. If the control is closed and then re-opened, the most recent measurement is displayed. A measurement can be modified by clicking and dragging on it. -{{% /admonition %}} -#### Length +##### Length Get the spherical length of a geometry. This length is the sum of the great circle distances between coordinates. For multi-part geometries, the length is the sum of the length of each part. Geometries are assumed to be in 'EPSG:3857'. @@ -687,9 +678,9 @@ Get the spherical length of a geometry. This length is the sum of the great circ - **Miles (mi)** - **Nautical miles (nmi)** -{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-measure-length-9-1-0.png" max-width="1200px" caption="Geomap panel measure length" >}} +{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-measure-length-9-1-0.png" max-width="1200px" alt="Geomap panel measure length" >}} -#### Area +##### Area Get the spherical area of a geometry. This area is calculated assuming that polygon edges are segments of great circles on a sphere. Geometries are assumed to be in 'EPSG:3857'. @@ -700,38 +691,38 @@ Get the spherical area of a geometry. This area is calculated assuming that poly - **Acres (acre)** - **Hectare (ha)** -{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-measure-area-9-1-0.png" max-width="1200px" caption="Geomap panel measure area" >}} +{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-measure-area-9-1-0.png" max-width="1200px" alt="Geomap panel measure area" >}} -### Show debug +#### Show debug Displays debug information in the upper right corner. This can be useful for debugging or validating a data source. - **Zoom** displays current zoom level of the map. - **Center** displays the current **longitude**, **latitude** of the map center. -{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-debug-9-1-0.png" max-width="1200px" caption="Geomap panel debug" >}} +{{< figure src="/static/img/docs/geomap-panel/geomap-map-controls-debug-9-1-0.png" max-width="1200px" alt="Geomap panel debug" >}} -### Tooltip +#### Tooltip - **None** displays tooltips only when a data point is clicked. - **Details** displays tooltips when a mouse pointer hovers over a data point. -## Standard options +### Standard options {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -## Data links +### Data links {{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}} -## Value mappings +### Value mappings {{< docs/shared lookup="visualizations/value-mappings-options.md" source="grafana" version="" >}} -## Thresholds +### Thresholds {{< docs/shared lookup="visualizations/thresholds-options-2.md" source="grafana" version="" >}} -## Field overrides +### Field overrides {{< docs/shared lookup="visualizations/overrides-options.md" source="grafana" version="" >}} diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 8e74e21186f..de2e43e06c3 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -964,6 +964,10 @@ This setting is ignored if multiple OAuth providers are configured. Default is ` How many seconds the OAuth state cookie lives before being deleted. Default is `600` (seconds) Administrators can increase this if they experience OAuth login state mismatch errors. +### oauth_login_error_message + +A custom error message for when users are unauthorized. Default is a key for an internationalized phrase in the frontend, `Login provider denied login request`. + ### oauth_refresh_token_server_lock_min_wait_ms Minimum wait time in milliseconds for the server lock retry mechanism. Default is `1000` (milliseconds). The server lock retry mechanism is used to prevent multiple Grafana instances from simultaneously refreshing OAuth tokens. This mechanism waits at least this amount of time before retrying to acquire the server lock. @@ -1090,6 +1094,16 @@ Set to `true` to enable verbose request signature logging when AWS Signature Ver
      +### managed_service_accounts_enabled + +> Only available in Grafana 11.3+. + +Set to `true` to enable the use of managed service accounts for plugin authentication. Default is `false`. + +> **Limitations:** +> This feature currently **only supports single-organization deployments**. +> The plugin's service account is automatically created in the default organization. This means the plugin can only access data and resources within that specific organization. + ## [auth.anonymous] Refer to [Anonymous authentication]({{< relref "../configure-security/configure-authentication/grafana#anonymous-authentication" >}}) for detailed instructions. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 39ab938fb28..82fe792d360 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -25,6 +25,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | | `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | | `publicDashboards` | [Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version. | Yes | +| `publicDashboardsScene` | Enables public dashboard rendering using scenes | Yes | | `featureHighlights` | Highlight Grafana Enterprise features | | | `correlations` | Correlations page | Yes | | `autoMigrateXYChartPanel` | Migrate old XYChart panel to new XYChart2 model | Yes | @@ -56,6 +57,9 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `managedPluginsInstall` | Install managed plugins directly from plugins catalog | Yes | | `addFieldFromCalculationStatFunctions` | Add cumulative and window functions to the add field from calculation transformation | Yes | | `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes | +| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes | +| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes | +| `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes | | `ssoSettingsApi` | Enables the SSO settings API and the OAuth configuration UIs in Grafana | Yes | | `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes | | `exploreMetrics` | Enables the new Explore Metrics core app | Yes | @@ -68,7 +72,9 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `tlsMemcached` | Use TLS-enabled memcached in the enterprise caching feature | Yes | | `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes | | `newDashboardSharingComponent` | Enables the new sharing drawer design | | +| `notificationBanner` | Enables the notification banner UI and API | Yes | | `pluginProxyPreserveTrailingSlash` | Preserve plugin proxy trailing slash. | | +| `pinNavItems` | Enables pinning of nav items | Yes | | `openSearchBackendFlowEnabled` | Enables the backend query flow for Open Search datasource plugin | Yes | | `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes | @@ -113,97 +119,93 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- [Experimental](https://grafana.com/docs/release-life-cycle/#experimental) features are early in their development lifecycle and so are not yet supported in Grafana Cloud. Experimental features might be changed or removed without prior notice. -| Feature toggle name | Description | -| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | -| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | -| `publicDashboardsScene` | Enables public dashboard rendering using scenes | -| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | -| `storage` | Configurable storage for dashboards, datasources, and resources | -| `canvasPanelNesting` | Allow elements nesting | -| `vizActions` | Allow actions in visualizations | -| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | -| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | -| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | -| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | -| `mysqlParseTime` | Ensure the parseTime flag is set for MySQL driver | -| `alertingBacktesting` | Rule backtesting API for alerting | -| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | -| `individualCookiePreferences` | Support overriding cookie preferences per user | -| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | -| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | -| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | -| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | -| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | -| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | -| `unifiedRequestLog` | Writes error logs to the request logger | -| `extraThemes` | Enables extra themes | -| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | -| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | -| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | -| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | -| `vizAndWidgetSplit` | Split panels between visualizations and widgets | -| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | -| `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `metricsSummary` | Enables metrics summary queries in the Tempo data source | -| `datasourceAPIServers` | Expose some datasources as apiservers. | -| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | -| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | -| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | -| `libraryPanelRBAC` | Enables RBAC support for library panels | -| `wargamesTesting` | Placeholder feature flag for internal testing | -| `externalCorePlugins` | Allow core plugins to be loaded as external | -| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | -| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | -| `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 | -| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders 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 | -| `queryServiceFromUI` | Routes requests to the new query service | -| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | -| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | -| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | -| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | -| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | -| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | -| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | -| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | -| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | -| `dashboardScene` | Enables dashboard rendering using scenes for all roles | -| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | -| `tableSharedCrosshair` | Enables shared crosshair in table panel | -| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | -| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | -| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | -| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | -| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | -| `expressionParser` | Enable new expression parser | -| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | -| `queryLibrary` | Enables Query Library feature in Explore | -| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | -| `alertingListViewV2` | Enables the new alert list view design | -| `notificationBanner` | Enables the notification banner UI and API | -| `dashboardRestore` | Enables deleted dashboard restore feature (backend only) | -| `alertingCentralAlertHistory` | Enables the new central alert history. | -| `pinNavItems` | Enables pinning of nav items | -| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | -| `alertingApiServer` | Register Alerting APIs with the K8s API server | -| `dashboardRestoreUI` | Enables the frontend to be able to restore a recently deleted dashboard | -| `dataplaneAggregator` | Enable grafana dataplane aggregator | -| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | -| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | -| `singleTopNav` | Unifies the top search bar and breadcrumb bar into one | -| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | -| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | -| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | -| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | -| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | -| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions | -| `rolePickerDrawer` | Enables the new role picker drawer design | +| Feature toggle name | Description | +| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | +| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | +| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | +| `storage` | Configurable storage for dashboards, datasources, and resources | +| `canvasPanelNesting` | Allow elements nesting | +| `vizActions` | Allow actions in visualizations | +| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | +| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | +| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | +| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | +| `mysqlParseTime` | Ensure the parseTime flag is set for MySQL driver | +| `alertingBacktesting` | Rule backtesting API for alerting | +| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | +| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | +| `individualCookiePreferences` | Support overriding cookie preferences per user | +| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | +| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | +| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | +| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | +| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | +| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | +| `unifiedRequestLog` | Writes error logs to the request logger | +| `extraThemes` | Enables extra themes | +| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | +| `pluginsFrontendSandbox` | Enables the plugins frontend sandbox | +| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | +| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | +| `vizAndWidgetSplit` | Split panels between visualizations and widgets | +| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | +| `mlExpressions` | Enable support for Machine Learning in server-side expressions | +| `metricsSummary` | Enables metrics summary queries in the Tempo data source | +| `datasourceAPIServers` | Expose some datasources as apiservers. | +| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | +| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | +| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | +| `libraryPanelRBAC` | Enables RBAC support for library panels | +| `wargamesTesting` | Placeholder feature flag for internal testing | +| `externalCorePlugins` | Allow core plugins to be loaded as external | +| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | +| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | +| `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 | +| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s | +| `grafanaAPIServerTestingWithExperimentalAPIs` | Facilitate integration testing of experimental APIs | +| `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 | +| `queryServiceFromUI` | Routes requests to the new query service | +| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | +| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | +| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | +| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | +| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | +| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | +| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | +| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | +| `tableSharedCrosshair` | Enables shared crosshair in table panel | +| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | +| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | +| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | +| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | +| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | +| `expressionParser` | Enable new expression parser | +| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | +| `queryLibrary` | Enables Query Library feature in Explore | +| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | +| `alertingListViewV2` | Enables the new alert list view design | +| `dashboardRestore` | Enables deleted dashboard restore feature (backend only) | +| `alertingCentralAlertHistory` | Enables the new central alert history. | +| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | +| `alertingApiServer` | Register Alerting APIs with the K8s API server | +| `dashboardRestoreUI` | Enables the frontend to be able to restore a recently deleted dashboard | +| `dataplaneAggregator` | Enable grafana dataplane aggregator | +| `newFiltersUI` | Enables new combobox style UI for the Ad hoc filters variable in scenes architecture | +| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | +| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | +| `singleTopNav` | Unifies the top search bar and breadcrumb bar into one | +| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | +| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | +| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | +| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | +| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | +| `alertingQueryAndExpressionsStepMode` | Enables step mode for alerting queries and expressions | +| `rolePickerDrawer` | Enables the new role picker drawer design | ## Development feature toggles diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md index 72d7cd80eb9..c8590e8c111 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/_index.md @@ -22,13 +22,13 @@ The following table shows all supported authentication providers and the feature | Provider | Multi Org Mapping | Enforce Sync | Role Mapping | Grafana Admin Mapping | Team Sync | Allowed groups | Active Sync | Skip OrgRole mapping | Auto Login | Single Logout | | :---------------------------------------------------- | :---------------- | :----------- | :----------- | :-------------------- | :-------- | :------------- | :---------- | :------------------- | :--------- | :------------ | | [Auth Proxy]({{< relref "./auth-proxy" >}}) | no | yes | yes | no | yes | no | N/A | no | N/A | N/A | -| [Azure AD OAuth]({{< relref "./azuread" >}}) | no | yes | yes | yes | yes | yes | N/A | yes | yes | yes | -| [Generic OAuth]({{< relref "./generic-oauth" >}}) | no | yes | yes | yes | yes | no | N/A | yes | yes | yes | -| [GitHub OAuth]({{< relref "./github" >}}) | no | yes | yes | yes | yes | yes | N/A | yes | yes | yes | -| [GitLab OAuth]({{< relref "./gitlab" >}}) | no | yes | yes | yes | yes | yes | N/A | yes | yes | yes | -| [Google OAuth]({{< relref "./google" >}}) | no | no | no | no | yes | no | N/A | no | yes | yes | +| [Azure AD OAuth]({{< relref "./azuread" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [Generic OAuth]({{< relref "./generic-oauth" >}}) | yes | yes | yes | yes | yes | no | N/A | yes | yes | yes | +| [GitHub OAuth]({{< relref "./github" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [GitLab OAuth]({{< relref "./gitlab" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [Google OAuth]({{< relref "./google" >}}) | yes | no | no | no | yes | no | N/A | no | yes | yes | | [Grafana.com OAuth]({{< relref "./grafana-cloud" >}}) | no | no | yes | no | N/A | N/A | N/A | yes | yes | yes | -| [Okta OAuth]({{< relref "./okta" >}}) | no | yes | yes | yes | yes | yes | N/A | yes | yes | yes | +| [Okta OAuth]({{< relref "./okta" >}}) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | | [SAML]({{< relref "./saml" >}}) (Enterprise only) | yes | yes | yes | yes | yes | yes | N/A | yes | yes | yes | | [LDAP]({{< relref "./ldap" >}}) | yes | yes | yes | yes | yes | yes | yes | no | N/A | N/A | | [JWT Proxy]({{< relref "./jwt" >}}) | no | yes | yes | yes | no | no | N/A | no | N/A | N/A | diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md index 91b0e40eec5..fa56bac67b5 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/generic-oauth/index.md @@ -313,7 +313,7 @@ Payload: "roles": [ "org_foo", "org_bar", - "another_org' + "another_org" ], ... }, diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md index bddba3548cd..29ef9f04975 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md @@ -280,13 +280,13 @@ Grafana supports user authentication through Okta, which is useful when you want - In the **Single sign on URL** field, use the `/saml/acs` endpoint URL of your Grafana instance, for example, `https://grafana.example.com/saml/acs`. - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, for example, `https://grafana.example.com/saml/metadata`. - Leave the default values for **Name ID format** and **Application username**. - - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana, for example: + - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana. The attribute names in Okta need to match exactly what is defined within Grafana, for example: - | Attribute name (in Grafana) | Value (in Okta profile) | - | --------------------------- | -------------------------------------- | - | Login | `user.login` | - | Email | `user.email` | - | DisplayName | `user.firstName + " " + user.lastName` | + | Attribute name (in Grafana) | Name and value (in Okta profile) | + | --------------------------- | -------------------------------------------------- | + | Login | Login `user.login` | + | Email | Email `user.email` | + | DisplayName | DisplayName `user.firstName + " " + user.lastName` | - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`) and set filter to `Matches regex .*` to return all user groups. diff --git a/docs/sources/setup-grafana/configure-security/export-logs.md b/docs/sources/setup-grafana/configure-security/export-logs.md index 907c980a4f9..ba942393530 100644 --- a/docs/sources/setup-grafana/configure-security/export-logs.md +++ b/docs/sources/setup-grafana/configure-security/export-logs.md @@ -32,7 +32,11 @@ Usage insights logs are JSON objects that represent certain user activities, suc ### Scope -A log is created every time a user opens a dashboard or when a query is sent to a data source in the dashboard view. A query that is performed via Explore does not generate a log. +A log is created every time: + +- A user opens a dashboard. +- A query is sent to a data source in the dashboard view. +- A query is performed via Explore. ### Format diff --git a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md index 34d220c51b2..c5ceb9e07dd 100644 --- a/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md +++ b/docs/sources/setup-grafana/image-rendering/troubleshooting/index.md @@ -97,6 +97,14 @@ On a minimal CentOS 8 installation, the following dependencies are required for libXcomposite libXdamage libXtst cups libXScrnSaver pango atk adwaita-cursor-theme adwaita-icon-theme at at-spi2-atk at-spi2-core cairo-gobject colord-libs dconf desktop-file-utils ed emacs-filesystem gdk-pixbuf2 glib-networking gnutls gsettings-desktop-schemas gtk-update-icon-cache gtk3 hicolor-icon-theme jasper-libs json-glib libappindicator-gtk3 libdbusmenu libdbusmenu-gtk3 libepoxy liberation-fonts liberation-narrow-fonts liberation-sans-fonts liberation-serif-fonts libgusb libindicator-gtk3 libmodman libproxy libsoup libwayland-cursor libwayland-egl libxkbcommon m4 mailx nettle patch psmisc redhat-lsb-core redhat-lsb-submod-security rest spax time trousers xdg-utils xkeyboard-config alsa-lib libX11-xcb ``` +**RHEL:** + +On a minimal RHEL 8 installation, the following dependencies are required for the image rendering to function: + +```bash +linux-vdso.so.1 libdl.so.2 libpthread.so.0 libgobject-2.0.so.0 libglib-2.0.so.0 libnss3.so libnssutil3.so libsmime3.so libnspr4.so libatk-1.0.so.0 libatk-bridge-2.0.so.0 libcups.so.2 libgio-2.0.so.0 libdrm.so.2 libdbus-1.so.3 libexpat.so.1 libxcb.so.1 libxkbcommon.so.0 libm.so.6 libX11.so.6 libXcomposite.so.1 libXdamage.so.1 libXext.so.6 libXfixes.so.3 libXrandr.so.2 libgbm.so.1 libpango-1.0.so.0 libcairo.so.2 libasound.so.2 libatspi.so.0 libgcc_s.so.1 libc.so.6 /lib64/ld-linux-x86-64.so.2 libgnutls.so.30 libpcre.so.1 libffi.so.6 libplc4.so libplds4.so librt.so.1 libgmodule-2.0.so.0 libgssapi_krb5.so.2 libkrb5.so.3 libk5crypto.so.3 libcom_err.so.2 libavahi-common.so.3 libavahi-client.so.3 libcrypt.so.1 libz.so.1 libselinux.so.1 libresolv.so.2 libmount.so.1 libsystemd.so.0 libXau.so.6 libXrender.so.1 libthai.so.0 libfribidi.so.0 libpixman-1.so.0 libfontconfig.so.1 libpng16.so.16 libxcb-render.so.0 libidn2.so.0 libunistring.so.2 libtasn1.so.6 libnettle.so.6 libhogweed.so.4 libgmp.so.10 libkrb5support.so.0 libkeyutils.so.1 libpcre2-8.so.0 libuuid.so.1 liblz4.so.1 libgcrypt.so.20 libbz2.so.1 +``` + ## Certificate signed by internal certificate authorities In many cases, Grafana runs on internal servers and uses certificates that have not been signed by a CA ([Certificate Authority](https://en.wikipedia.org/wiki/Certificate_authority)) known to Chrome, and therefore cannot be validated. Chrome internally uses NSS ([Network Security Services](https://en.wikipedia.org/wiki/Network_Security_Services)) for cryptographic operations such as the validation of certificates. @@ -125,6 +133,22 @@ If this happens, then you have to add the certificate to the trust store. If you certutil –addstore "Root" /internal-root-ca.crt.pem ``` +**Container:** + +```Dockerfile +FROM grafana/grafana-image-renderer:latest + +USER root + +RUN apk add --no-cache nss-tools + +USER grafana + +COPY internal-root-ca.crt.pem /etc/pki/tls/certs/internal-root-ca.crt.pem +RUN mkdir -p /home/grafana/.pki/nssdb +RUN certutil -d sql:/home/grafana/.pki/nssdb -A -n internal-root-ca -t C -i /etc/pki/tls/certs/internal-root-ca.crt.pem +``` + ## Custom Chrome/Chromium As a last resort, if you already have [Chrome](https://www.google.com/chrome/) or [Chromium](https://www.chromium.org/) diff --git a/docs/sources/setup-grafana/start-restart-grafana.md b/docs/sources/setup-grafana/start-restart-grafana.md index 036350e12e3..49f99a2cf30 100644 --- a/docs/sources/setup-grafana/start-restart-grafana.md +++ b/docs/sources/setup-grafana/start-restart-grafana.md @@ -33,12 +33,11 @@ Complete the following steps to start the Grafana server using systemd and verif ```bash sudo systemctl daemon-reload sudo systemctl start grafana-server - sudo systemctl status grafana-server ``` 1. To verify that the service is running, run the following command: - ``` + ```bash sudo systemctl status grafana-server ``` @@ -56,7 +55,7 @@ sudo systemctl enable grafana-server.service ### Restart the Grafana server using systemd -To restart the Grafana server, run the following commands: +To restart the Grafana server, run the following command: ```bash sudo systemctl restart grafana-server @@ -70,16 +69,15 @@ SUSE or openSUSE users might need to start the server with the systemd method, t Complete the following steps to start the Grafana server using init.d and verify that it is running: -1. To start the Grafana server, run the following commands: +1. To start the Grafana server, run the following command: ```bash sudo service grafana-server start - sudo service grafana-server status ``` 1. To verify that the service is running, run the following command: - ``` + ```bash sudo service grafana-server status ``` @@ -93,7 +91,7 @@ sudo update-rc.d grafana-server defaults #### Restart the Grafana server using init.d -To restart the Grafana server, run the following commands: +To restart the Grafana server, run the following command: ```bash sudo service grafana-server restart @@ -121,8 +119,8 @@ Alternatively, you can use the `docker compose restart` command to restart Grafa Configure your `docker-compose.yml` file. For example: -```bash -version: "3.8" +```yml +version: '3.8' services: grafana: image: grafana/grafana:latest diff --git a/docs/sources/shared/systemd/bind-net-capabilities.md b/docs/sources/shared/systemd/bind-net-capabilities.md index 89a07a1cfbc..a8d3c5d5060 100644 --- a/docs/sources/shared/systemd/bind-net-capabilities.md +++ b/docs/sources/shared/systemd/bind-net-capabilities.md @@ -19,7 +19,7 @@ If you are using `systemd` and want to start Grafana on a port that is lower tha To learn more about capabilities, refer to [capabilities(7) — Linux manual page](https://man7.org/linux/man-pages/man7/capabilities.7.html). - ``` + ```ini [Service] # Give the CAP_NET_BIND_SERVICE capability CapabilityBoundingSet=CAP_NET_BIND_SERVICE diff --git a/docs/sources/tutorials/alerting-get-started/index.md b/docs/sources/tutorials/alerting-get-started/index.md index 1b7ea42a167..7ebea1418eb 100644 --- a/docs/sources/tutorials/alerting-get-started/index.md +++ b/docs/sources/tutorials/alerting-get-started/index.md @@ -49,31 +49,43 @@ Before you dive in, remember that you can [explore advanced topics like alert in {{< /docs/ignore >}} - + + +{{< docs/ignore >}} + +## Set up the Grafana stack + +{{< /docs/ignore >}} ## Before you begin -### Grafana Cloud users +There are different ways you can follow along with this tutorial. -As a Grafana Cloud user, you don't have to install anything. +### Grafana Cloud - +As a Grafana Cloud user, you don't have to install anything. [Create your free account](http://grafana.com/auth/sign-up/create-user). Continue to [Create a contact point](#create-a-contact-point). - +### Interactive learning environment -### Grafana OSS users +Alternatively, you can try out this example in our interactive learning environment: [Get started with Grafana Alerting](https://killercoda.com/grafana-labs/course/grafana/alerting-get-started/). -In order to run a Grafana stack locally, ensure you have the following applications installed. +It's a fully configured environment with all the dependencies already installed. + +### Grafana OSS + +If you opt to run a Grafana stack locally, ensure you have the following applications installed: - [Docker Compose](https://docs.docker.com/get-docker/) (included in Docker for Desktop for macOS and Windows) - [Git](https://git-scm.com/) -#### Set up the Grafana Stack (OSS users) +#### Set up the Grafana stack (OSS users) -To demonstrate the observation of data using the Grafana stack, download the files to your local machine. + + +To demonstrate the observation of data using the Grafana stack, download and run the following files. 1. Clone the [tutorial environment repository](https://www.github.com/grafana/tutorial-environment). @@ -135,19 +147,6 @@ To demonstrate the observation of data using the Grafana stack, download the fil {{< /docs/ignore >}} - - - {{< admonition type="tip" >}} - Alternatively, you can try out this example in our interactive learning environment: [Get started with Grafana Alerting](https://killercoda.com/grafana-labs/course/grafana/alerting-get-started/). - - It's a fully configured environment with all the dependencies already installed. - - ![Interactive](/media/docs/grafana/full-stack-ile.png) - - Provide feedback, report bugs, and raise issues in the [Grafana Killercoda repository](https://github.com/grafana/killercoda). - {{< /admonition >}} - - @@ -223,10 +222,11 @@ In this section, we define queries, expressions (used to manipulate the data), a ### Set evaluation behavior -An [evaluation group](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/) defines when an alert rule fires, and it’s based on two settings: +The [alert rule evaluation](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/) defines the conditions under which an alert rule triggers, based on the following settings: -- **Evaluation group**: how frequently the alert rule is evaluated. -- **Evaluation interval**: how long the condition must be met to start firing. This allows your data time to stabilize before triggering an alert, helping to reduce the frequency of unnecessary notifications. +- **Evaluation group**: every alert rule is assigned to an evaluation group. You can assign the alert rule to an existing evaluation group or create a new one. +- **Evaluation interval**: determines how frequently the alert rule is checked. For instance, the evaluation may occur every 10s, 30s, 1m, 10m, etc. +- **Pending period**: how long the condition must be met to trigger the alert rule. To set up the evaluation: diff --git a/e2e/cypress/support/e2e.js b/e2e/cypress/support/e2e.js index 39bab3d3c21..3e03d0f4344 100644 --- a/e2e/cypress/support/e2e.js +++ b/e2e/cypress/support/e2e.js @@ -46,8 +46,8 @@ Cypress.on('uncaught:exception', (err) => { // beforeEach(() => { - if (Cypress.env('SCENES')) { - cy.logToConsole('enabling dashboardScene feature toggle in localstorage'); - cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=true'); + if (Cypress.env('DISABLE_SCENES')) { + cy.logToConsole('disabling dashboardScene feature toggle in localstorage'); + cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=false'); } }); diff --git a/e2e/dashboards-suite/Repeating_a_panel_horizontally.spec.ts b/e2e/dashboards-suite/Repeating_a_panel_horizontally.spec.ts index e34acc89d81..9b3ec4806e0 100644 --- a/e2e/dashboards-suite/Repeating_a_panel_horizontally.spec.ts +++ b/e2e/dashboards-suite/Repeating_a_panel_horizontally.spec.ts @@ -37,9 +37,15 @@ describe('Repeating a panel horizontally', () => { }); // Change to only show panels 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal') + .parent() + .within(() => { + cy.get('input').click(); + }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); + // blur the dropdown cy.get('body').click(); diff --git a/e2e/dashboards-suite/Repeating_a_panel_vertically.spec.ts b/e2e/dashboards-suite/Repeating_a_panel_vertically.spec.ts index 2b6a51f0515..fbf0f5bf9d2 100644 --- a/e2e/dashboards-suite/Repeating_a_panel_vertically.spec.ts +++ b/e2e/dashboards-suite/Repeating_a_panel_vertically.spec.ts @@ -38,9 +38,15 @@ describe('Repeating a panel vertically', () => { }); // Change to only show panels 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical') + .parent() + .within(() => { + cy.get('input').click(); + }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); + // blur the dropdown cy.get('body').click(); diff --git a/e2e/dashboards-suite/Repeating_an_empty_row.spec.ts b/e2e/dashboards-suite/Repeating_an_empty_row.spec.ts index 8777ea3e065..2bf52250bad 100644 --- a/e2e/dashboards-suite/Repeating_an_empty_row.spec.ts +++ b/e2e/dashboards-suite/Repeating_an_empty_row.spec.ts @@ -32,10 +32,15 @@ describe('Repeating empty rows', () => { e2e.components.DashboardRow.title(title).should('be.visible'); }); - // Change to only show rows 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('row').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('row') + .parent() + .within(() => { + cy.get('input').click(); + }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); + // blur the dropdown cy.get('body').click(); diff --git a/e2e/dashboards-suite/dashboard-browse.spec.ts b/e2e/dashboards-suite/dashboard-browse.spec.ts index 4e9728b7a65..a5dc3cfc5ca 100644 --- a/e2e/dashboards-suite/dashboard-browse.spec.ts +++ b/e2e/dashboards-suite/dashboard-browse.spec.ts @@ -1,7 +1,7 @@ import testDashboard from '../dashboards/TestDashboard.json'; import { e2e } from '../utils'; - -describe('Dashboard browse', () => { +// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-browse.spec.ts +describe.skip('Dashboard browse', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); diff --git a/e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts b/e2e/dashboards-suite/dashboard-export-json.spec.ts similarity index 98% rename from e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts rename to e2e/dashboards-suite/dashboard-export-json.spec.ts index 6795c5680a7..34abe6cec05 100644 --- a/e2e/scenes/dashboards-suite/dashboard-export-json.spec.ts +++ b/e2e/dashboards-suite/dashboard-export-json.spec.ts @@ -1,5 +1,5 @@ import { e2e } from '../utils'; -import '../../utils/support/clipboard'; +import '../utils/support/clipboard'; describe('Export as JSON', () => { beforeEach(() => { diff --git a/e2e/scenes/dashboards-suite/dashboard-keybindings.spec.ts b/e2e/dashboards-suite/dashboard-keybindings.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/dashboard-keybindings.spec.ts rename to e2e/dashboards-suite/dashboard-keybindings.spec.ts diff --git a/e2e/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e/dashboards-suite/dashboard-live-streaming.spec.ts index b91ccf5c270..089b3ec4beb 100644 --- a/e2e/dashboards-suite/dashboard-live-streaming.spec.ts +++ b/e2e/dashboards-suite/dashboard-live-streaming.spec.ts @@ -1,7 +1,8 @@ import testDashboard from '../dashboards/DashboardLiveTest.json'; import { e2e } from '../utils'; -describe('Dashboard Live streaming support', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/dashboard-live-streaming.spec.ts +describe.skip('Dashboard Live streaming support', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.importDashboard(testDashboard, 1000); diff --git a/e2e/dashboards-suite/dashboard-public-create.spec.ts b/e2e/dashboards-suite/dashboard-public-create.spec.ts index 6fa08c988e5..993bb82c702 100644 --- a/e2e/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-public-create.spec.ts @@ -1,6 +1,6 @@ import { e2e } from '../utils'; - -describe('Public dashboards', () => { +// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-public-create.spec.ts +describe.skip('Public dashboards', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -14,7 +14,7 @@ describe('Public dashboards', () => { cy.wait('@query'); // Open sharing modal - e2e.pages.Dashboard.DashNav.shareButton().click(); + e2e.components.NavToolbar.shareDashboard().click(); // Select public dashboards tab e2e.components.Tab.title('Public dashboard').click(); @@ -74,7 +74,7 @@ describe('Public dashboards', () => { e2e.pages.Dashboard.DashNav.publicDashboardTag().should('exist'); // Open sharing modal - e2e.pages.Dashboard.DashNav.shareButton().click(); + e2e.components.NavToolbar.shareDashboard().click(); // Select public dashboards tab cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); @@ -114,7 +114,7 @@ describe('Public dashboards', () => { cy.wait('@query'); // Open sharing modal - e2e.pages.Dashboard.DashNav.shareButton().click(); + e2e.components.NavToolbar.shareDashboard().click(); // Select public dashboards tab cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); diff --git a/e2e/dashboards-suite/dashboard-public-templating.spec.ts b/e2e/dashboards-suite/dashboard-public-templating.spec.ts index fa3a4a91242..4a4e660d44e 100644 --- a/e2e/dashboards-suite/dashboard-public-templating.spec.ts +++ b/e2e/dashboards-suite/dashboard-public-templating.spec.ts @@ -10,10 +10,10 @@ describe('Create a public dashboard with template variables shows a template var e2e.flows.openDashboard({ uid: 'HYaGDGIMk' }); // Open sharing modal - e2e.pages.Dashboard.DashNav.shareButton().click(); + e2e.components.NavToolbar.shareDashboard().click(); // Select public dashboards tab - e2e.components.Tab.title('Public dashboard').click(); + e2e.components.Tab.title('Public Dashboard').click(); // Warning Alert dashboard cannot be made public because it has template variables e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts b/e2e/dashboards-suite/dashboard-share-externally-create.spec.ts similarity index 97% rename from e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts rename to e2e/dashboards-suite/dashboard-share-externally-create.spec.ts index 60642f9e0ef..39e01c4e60f 100644 --- a/e2e/scenes/dashboards-suite/dashboard-share-externally-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-share-externally-create.spec.ts @@ -1,6 +1,6 @@ -import { PublicDashboard } from '../../../public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; +import { PublicDashboard } from '../../public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { e2e } from '../utils'; -import '../../utils/support/clipboard'; +import '../utils/support/clipboard'; describe('Shared dashboards', () => { beforeEach(() => { diff --git a/e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts b/e2e/dashboards-suite/dashboard-share-internally.spec.ts similarity index 98% rename from e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts rename to e2e/dashboards-suite/dashboard-share-internally.spec.ts index 96b7291d40f..76773fa7f71 100644 --- a/e2e/scenes/dashboards-suite/dashboard-share-internally.spec.ts +++ b/e2e/dashboards-suite/dashboard-share-internally.spec.ts @@ -1,6 +1,6 @@ -import { ShareLinkConfiguration } from '../../../public/app/features/dashboard-scene/sharing/ShareButton/utils'; +import { ShareLinkConfiguration } from '../../public/app/features/dashboard-scene/sharing/ShareButton/utils'; import { e2e } from '../utils'; -import '../../utils/support/clipboard'; +import '../utils/support/clipboard'; describe('Share internally', () => { beforeEach(() => { diff --git a/e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts b/e2e/dashboards-suite/dashboard-share-snapshot-create.spec.ts similarity index 93% rename from e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts rename to e2e/dashboards-suite/dashboard-share-snapshot-create.spec.ts index 808ebc69ee8..6661750718e 100644 --- a/e2e/scenes/dashboards-suite/dashboard-share-snapshot-create.spec.ts +++ b/e2e/dashboards-suite/dashboard-share-snapshot-create.spec.ts @@ -1,7 +1,7 @@ -import { SnapshotCreateResponse } from '../../../public/app/features/dashboard/services/SnapshotSrv'; -import { fromBaseUrl } from '../../utils/support/url'; +import { SnapshotCreateResponse } from '../../public/app/features/dashboard/services/SnapshotSrv'; import { e2e } from '../utils'; -import '../../utils/support/clipboard'; +import { fromBaseUrl } from '../utils/support/url'; +import '../utils/support/clipboard'; describe('Snapshots', () => { beforeEach(() => { diff --git a/e2e/dashboards-suite/dashboard-templating.spec.ts b/e2e/dashboards-suite/dashboard-templating.spec.ts index 71b9fcbf7db..4e885df1258 100644 --- a/e2e/dashboards-suite/dashboard-templating.spec.ts +++ b/e2e/dashboards-suite/dashboard-templating.spec.ts @@ -34,7 +34,7 @@ describe('Dashboard templating', () => { `Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`, `Server:date = NaN`, `Server:text = All`, - `Server:queryparam = var-Server=All`, + `Server:queryparam = var-Server=A%27A%22A&var-Server=BB%5CB&var-Server=CCC`, `1 < 2`, `Example: from=now-6h&to=now`, ]; diff --git a/e2e/dashboards-suite/dashboard-time-zone.spec.ts b/e2e/dashboards-suite/dashboard-time-zone.spec.ts index 3c2844188e3..0361e84a236 100644 --- a/e2e/dashboards-suite/dashboard-time-zone.spec.ts +++ b/e2e/dashboards-suite/dashboard-time-zone.spec.ts @@ -81,7 +81,8 @@ describe('Dashboard time zone support', () => { } }); - it('Tests relative timezone support and overrides', () => { + // TODO: remove skip once https://github.com/grafana/grafana/issues/86420 is done + it.skip('Tests relative timezone support and overrides', () => { // Open dashboard e2e.flows.openDashboard({ uid: 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8', @@ -123,7 +124,6 @@ describe('Dashboard time zone support', () => { .within(() => { cy.contains('[role="row"]', '00:00:00').should('be.visible'); }); - // Test UTC timezone e2e.flows.setTimeRange({ from: 'now-6h', diff --git a/e2e/dashboards-suite/general-dashboards.spec.ts b/e2e/dashboards-suite/general-dashboards.spec.ts index 1d01c82267c..bc1b2a306be 100644 --- a/e2e/dashboards-suite/general-dashboards.spec.ts +++ b/e2e/dashboards-suite/general-dashboards.spec.ts @@ -22,9 +22,9 @@ describe('Dashboards', () => { // Then we open and close the panel editor e2e.components.Panels.Panel.menu('Panel #50').click({ force: true }); // it only shows on hover e2e.components.Panels.Panel.menuItems('Edit').click(); - e2e.components.PanelEditor.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); - // And the last panel should still be visible! + // The last panel should still be visible! e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); }); }); diff --git a/e2e/dashboards-suite/load-options-from-url.spec.ts b/e2e/dashboards-suite/load-options-from-url.spec.ts index c1678a0aa40..d674ae2a385 100644 --- a/e2e/dashboards-suite/load-options-from-url.spec.ts +++ b/e2e/dashboards-suite/load-options-from-url.spec.ts @@ -16,40 +16,50 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB').should('be.visible'); @@ -65,40 +75,50 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); @@ -125,24 +145,25 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + cy.get('body').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') .should('be.visible') - .within(() => { - e2e.components.Variables.variableOption().should('have.length', 65); - }); + .should('have.length', 2); }); }); diff --git a/e2e/dashboards-suite/new-constant-variable.spec.ts b/e2e/dashboards-suite/new-constant-variable.spec.ts index 5cb43e21359..42693acb668 100644 --- a/e2e/dashboards-suite/new-constant-variable.spec.ts +++ b/e2e/dashboards-suite/new-constant-variable.spec.ts @@ -9,7 +9,7 @@ describe('Variables - Constant', () => { }); it('can add a new constant variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Constant" variable @@ -22,11 +22,11 @@ describe('Variables - Constant', () => { e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type('pesto').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto'); + // e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto'); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.components.RefreshPicker.runButtonV2().click(); // Assert it was rendered diff --git a/e2e/dashboards-suite/new-custom-variable.spec.ts b/e2e/dashboards-suite/new-custom-variable.spec.ts index 7b75622aff7..bda748fbdf2 100644 --- a/e2e/dashboards-suite/new-custom-variable.spec.ts +++ b/e2e/dashboards-suite/new-custom-variable.spec.ts @@ -25,7 +25,7 @@ describe('Variables - Custom', () => { }); it('can add a custom template variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Custom" variable @@ -34,17 +34,16 @@ describe('Variables - Custom', () => { assertPreviewValues(['one', 'two', 'three']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('two').click(); - + e2e.components.Select.option().contains('two').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: two'); }); it('can add a custom template variable with labels', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Custom" variable @@ -58,10 +57,10 @@ describe('Variables - Custom', () => { assertPreviewValues(['One', 'Two', 'Three']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('One').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('Two').click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1').click(); + e2e.components.Select.option().contains('Two').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 2'); diff --git a/e2e/dashboards-suite/new-datasource-variable.spec.ts b/e2e/dashboards-suite/new-datasource-variable.spec.ts index 40260a76520..b56e458567c 100644 --- a/e2e/dashboards-suite/new-datasource-variable.spec.ts +++ b/e2e/dashboards-suite/new-datasource-variable.spec.ts @@ -3,16 +3,14 @@ import { e2e } from '../utils'; const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; const DASHBOARD_NAME = 'Test variable output'; -const gdev_mysql = 'gdev-mysql'; -const gdev_mysql_ds_tests = 'gdev-mysql-ds-tests'; - -describe('Variables - Datasource', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/new-datasource-variable.spec.ts +describe.skip('Variables - Datasource', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); it('can add a new datasource variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Datasource" variable @@ -23,26 +21,30 @@ describe('Variables - Datasource', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); - // If this is failing, but sure to check there are MySQL datasources named "gdev-mysql" and "gdev-mysql-ds-tests" + // If this is failing, but sure to check there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus" // Or, just update is to match some gdev datasources to test with :) e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().within(() => { - cy.get('input').type('MySQL{enter}'); + cy.get('input').type('Prometheus{enter}'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('contain.text', gdev_mysql); e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should( 'contain.text', - gdev_mysql_ds_tests + 'gdev-prometheus' + ); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should( + 'contain.text', + 'gdev-slow-prometheus' ); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.components.RefreshPicker.runButtonV2().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(gdev_mysql).click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(gdev_mysql_ds_tests).click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus').click(); + e2e.components.Select.option().contains('gdev-slow-prometheus').click(); // Assert it was rendered - cy.get('.markdown-html').should('include.text', `VariableUnderTestText: ${gdev_mysql_ds_tests}`); + cy.get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus-uid'); + cy.get('.markdown-html').should('include.text', 'VariableUnderTestText: gdev-slow-prometheus'); }); }); diff --git a/e2e/dashboards-suite/new-interval-variable.spec.ts b/e2e/dashboards-suite/new-interval-variable.spec.ts index f99c7a2d8b2..88e1052ce5e 100644 --- a/e2e/dashboards-suite/new-interval-variable.spec.ts +++ b/e2e/dashboards-suite/new-interval-variable.spec.ts @@ -16,7 +16,7 @@ describe('Variables - Interval', () => { }); it('can add a new interval variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Interval" variable @@ -35,12 +35,13 @@ describe('Variables - Interval', () => { assertPreviewValues(['10s', '10m', '60m', '90m', '1h30m']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.components.RefreshPicker.runButtonV2().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('10s').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1h30m').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test').next().should('have.text', `10s`).click(); + e2e.components.Select.option().contains('1h30m').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 1h30m'); diff --git a/e2e/dashboards-suite/new-query-variable.spec.ts b/e2e/dashboards-suite/new-query-variable.spec.ts index c5edb437147..d6ead2937c4 100644 --- a/e2e/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/dashboards-suite/new-query-variable.spec.ts @@ -1,3 +1,5 @@ +import { selectors } from '@grafana/e2e-selectors'; + import { e2e } from '../utils'; const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; @@ -9,10 +11,12 @@ describe('Variables - Query - Add variable', () => { }); it('query variable should be default and default fields should be correct', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); + cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) + .should('be.visible') + .click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2() .should('be.visible') @@ -68,15 +72,17 @@ describe('Variables - Query - Add variable', () => { cy.get('input[type="checkbox"]').should('not.be.checked'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.have.text'); e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist'); }); it('adding a single value query variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); + cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) + .should('be.visible') + .click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') @@ -101,29 +107,27 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItem() .should('have.length', 4) .eq(3) .within(() => { - e2e.components.Variables.variableLinkWrapper().should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() - .should('be.visible') - .within(() => { - e2e.components.Variables.variableOption().should('have.length', 1); - }); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); + cy.get('input').click(); }); + + e2e.components.Select.option().should('have.length', 1); + e2e.components.Select.option().contains('C'); }); it('adding a multi value query variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); + cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) + .should('be.visible') + .click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') @@ -161,22 +165,21 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItem() .should('have.length', 4) .eq(3) .within(() => { - e2e.components.Variables.variableLinkWrapper().should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() - .should('be.visible') - .within(() => { - e2e.components.Variables.variableOption().should('have.length', 2); - }); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); + cy.get('input').click(); }); + + e2e.components.Select.option().should('have.length', 3); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + + e2e.components.Select.option().contains('All'); + e2e.components.Select.option().contains('C'); }); }); diff --git a/e2e/dashboards-suite/new-text-box-variable.spec.ts b/e2e/dashboards-suite/new-text-box-variable.spec.ts index b6cbabceef8..7da9afa32bf 100644 --- a/e2e/dashboards-suite/new-text-box-variable.spec.ts +++ b/e2e/dashboards-suite/new-text-box-variable.spec.ts @@ -9,26 +9,25 @@ describe('Variables - Text box', () => { }); it('can add a new text box variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "text box" variable e2e.components.CallToActionCard.buttonV2('Add variable').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => { - cy.get('input').type('Text box{enter}'); + cy.get('input').type('Textbox{enter}'); }); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2().type('cat-dog').blur(); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'cat-dog'); - // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); - e2e.pages.Dashboard.Settings.Actions.close().click(); - cy.get('#var-VariableUnderTest').clear().type('dog-cat').blur(); - + e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.SubMenu.submenuItem().within(() => { + cy.get('input').clear().type('dog-cat').blur(); + }); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: dog-cat'); }); diff --git a/e2e/dashboards-suite/set-options-from-ui.spec.ts b/e2e/dashboards-suite/set-options-from-ui.spec.ts index d7ec950487b..bdcc5c6b516 100644 --- a/e2e/dashboards-suite/set-options-from-ui.spec.ts +++ b/e2e/dashboards-suite/set-options-from-ui.spec.ts @@ -1,3 +1,5 @@ +import { selectors } from '@grafana/e2e-selectors'; + import { e2e } from '../utils'; const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; @@ -10,39 +12,46 @@ describe('Variables - Set options from ui', () => { it('clicking a value that is not part of dependents options should change these to All', () => { e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` }); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + .should('be.visible') + .within(() => { + cy.get('input').click(); + }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); - e2e.components.NavToolbar.container().click(); + cy.get('body').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') .should('have.length', 2) .eq(0) - .should('be.visible') - .click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().parent().should('have.length', 10); + + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); + cy.get('body').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() - .should('be.visible') + e2e.pages.Dashboard.SubMenu.submenuItemLabels('pod') + .parent() .within(() => { - e2e.components.Variables.variableOption().should('have.length', 65); + cy.get('input').click(); }); + // length is 11 because of virtualized select options + e2e.components.Select.option().parent().should('have.length', 11); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAB').should('be.visible'); @@ -60,41 +69,47 @@ describe('Variables - Set options from ui', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + .should('be.visible') + .within(() => { + cy.get('input').click(); + }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); - e2e.components.NavToolbar.container().click(); + e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (2)'); + + cy.get('body').click(); cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').scrollIntoView().should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B').scrollIntoView().should('be.visible'); - e2e.components.LoadingIndicator.icon().should('have.length', 0); + cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 17); + cy.get('input').click(); }); + e2e.components.Select.option().should('have.length', 11); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AD').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA').should('be.visible').click(); + cy.get('body').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().should('have.length', 10); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB').should('be.visible'); @@ -108,38 +123,46 @@ describe('Variables - Set options from ui', () => { cy.intercept({ pathname: '/api/ds/query' }).as('query'); cy.wait('@query'); + cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); - - e2e.components.NavToolbar.container().click(); - - cy.wait('@query'); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); - - e2e.components.LoadingIndicator.icon().should('have.length', 0); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); + + cy.get('body').click(); + + cy.wait('@query'); + cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').should('be.visible'); + + cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') + .should('be.visible') + .within(() => { + cy.get('input').click(); + }); + + e2e.components.Select.option().should('have.length', 10); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').should('be.visible').click(); + cy.get('body').click(0, 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') .should('be.visible') .within(() => { - e2e.components.Variables.variableOption().should('have.length', 9); + cy.get('input').click(); }); + e2e.components.Select.option().should('have.length', 10); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/snapshot-create.spec.ts b/e2e/dashboards-suite/snapshot-create.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/snapshot-create.spec.ts rename to e2e/dashboards-suite/snapshot-create.spec.ts diff --git a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index 06e13002db4..0aeadd56bcc 100644 --- a/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -27,21 +27,27 @@ describe('Templating', () => { expect(links).to.have.length.greaterThan(13); for (let index = 0; index < links.length; index++) { - expect(Cypress.$(links[index]).attr('href')).contains(`var-custom=${variableValue}`); + expect(Cypress.$(links[index]).attr('href')).contains(variableValue); } }); }; e2e.components.DashboardLinks.dropDown().should('be.visible').click().wait('@tagsTemplatingSearch'); - // verify all links, should have All value - verifyLinks('All'); + verifyLinks('var-custom=%24__all'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); + cy.get('body').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + .should('be.visible') + .within(() => { + cy.get('input').click(); + }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2').should('be.visible').click(); - e2e.components.NavToolbar.container().click(); + cy.get('body').click(); + e2e.components.DashboardLinks.dropDown() .scrollIntoView() .should('be.visible') diff --git a/e2e/scenes/dashboards-suite/Repeating_a_panel_horizontally.spec.ts b/e2e/old-arch/dashboards-suite/Repeating_a_panel_horizontally.spec.ts similarity index 95% rename from e2e/scenes/dashboards-suite/Repeating_a_panel_horizontally.spec.ts rename to e2e/old-arch/dashboards-suite/Repeating_a_panel_horizontally.spec.ts index 9b3ec4806e0..e34acc89d81 100644 --- a/e2e/scenes/dashboards-suite/Repeating_a_panel_horizontally.spec.ts +++ b/e2e/old-arch/dashboards-suite/Repeating_a_panel_horizontally.spec.ts @@ -37,15 +37,9 @@ describe('Repeating a panel horizontally', () => { }); // Change to only show panels 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal') - .parent() - .within(() => { - cy.get('input').click(); - }); - + e2e.pages.Dashboard.SubMenu.submenuItemLabels('horizontal').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); - // blur the dropdown cy.get('body').click(); diff --git a/e2e/scenes/dashboards-suite/Repeating_a_panel_vertically.spec.ts b/e2e/old-arch/dashboards-suite/Repeating_a_panel_vertically.spec.ts similarity index 95% rename from e2e/scenes/dashboards-suite/Repeating_a_panel_vertically.spec.ts rename to e2e/old-arch/dashboards-suite/Repeating_a_panel_vertically.spec.ts index fbf0f5bf9d2..2b6a51f0515 100644 --- a/e2e/scenes/dashboards-suite/Repeating_a_panel_vertically.spec.ts +++ b/e2e/old-arch/dashboards-suite/Repeating_a_panel_vertically.spec.ts @@ -38,15 +38,9 @@ describe('Repeating a panel vertically', () => { }); // Change to only show panels 1 + 3 - e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical') - .parent() - .within(() => { - cy.get('input').click(); - }); - + e2e.pages.Dashboard.SubMenu.submenuItemLabels('vertical').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); - // blur the dropdown cy.get('body').click(); diff --git a/e2e/scenes/dashboards-suite/Repeating_an_empty_row.spec.ts b/e2e/old-arch/dashboards-suite/Repeating_an_empty_row.spec.ts similarity index 94% rename from e2e/scenes/dashboards-suite/Repeating_an_empty_row.spec.ts rename to e2e/old-arch/dashboards-suite/Repeating_an_empty_row.spec.ts index 2bf52250bad..8777ea3e065 100644 --- a/e2e/scenes/dashboards-suite/Repeating_an_empty_row.spec.ts +++ b/e2e/old-arch/dashboards-suite/Repeating_an_empty_row.spec.ts @@ -32,15 +32,10 @@ describe('Repeating empty rows', () => { e2e.components.DashboardRow.title(title).should('be.visible'); }); - e2e.pages.Dashboard.SubMenu.submenuItemLabels('row') - .parent() - .within(() => { - cy.get('input').click(); - }); - + // Change to only show rows 1 + 3 + e2e.pages.Dashboard.SubMenu.submenuItemLabels('row').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('3').click(); - // blur the dropdown cy.get('body').click(); diff --git a/e2e/scenes/dashboards-suite/dashboard-browse-nested.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-browse-nested.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/dashboard-browse-nested.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-browse-nested.spec.ts diff --git a/e2e/scenes/dashboards-suite/dashboard-browse.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-browse.spec.ts similarity index 93% rename from e2e/scenes/dashboards-suite/dashboard-browse.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-browse.spec.ts index a5dc3cfc5ca..4e9728b7a65 100644 --- a/e2e/scenes/dashboards-suite/dashboard-browse.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-browse.spec.ts @@ -1,7 +1,7 @@ import testDashboard from '../dashboards/TestDashboard.json'; import { e2e } from '../utils'; -// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-browse.spec.ts -describe.skip('Dashboard browse', () => { + +describe('Dashboard browse', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); diff --git a/e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-live-streaming.spec.ts similarity index 75% rename from e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-live-streaming.spec.ts index 089b3ec4beb..b91ccf5c270 100644 --- a/e2e/scenes/dashboards-suite/dashboard-live-streaming.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-live-streaming.spec.ts @@ -1,8 +1,7 @@ import testDashboard from '../dashboards/DashboardLiveTest.json'; import { e2e } from '../utils'; -// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/dashboard-live-streaming.spec.ts -describe.skip('Dashboard Live streaming support', () => { +describe('Dashboard Live streaming support', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); e2e.flows.importDashboard(testDashboard, 1000); diff --git a/e2e/dashboards-suite/dashboard-panel-attention.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-panel-attention.spec.ts similarity index 100% rename from e2e/dashboards-suite/dashboard-panel-attention.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-panel-attention.spec.ts diff --git a/e2e/scenes/dashboards-suite/dashboard-public-create.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-public-create.spec.ts similarity index 95% rename from e2e/scenes/dashboards-suite/dashboard-public-create.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-public-create.spec.ts index 993bb82c702..6fa08c988e5 100644 --- a/e2e/scenes/dashboards-suite/dashboard-public-create.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-public-create.spec.ts @@ -1,6 +1,6 @@ import { e2e } from '../utils'; -// Skipping due to race conditions with same old arch test e2e/dashboards-suite/dashboard-public-create.spec.ts -describe.skip('Public dashboards', () => { + +describe('Public dashboards', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -14,7 +14,7 @@ describe.skip('Public dashboards', () => { cy.wait('@query'); // Open sharing modal - e2e.components.NavToolbar.shareDashboard().click(); + e2e.pages.Dashboard.DashNav.shareButton().click(); // Select public dashboards tab e2e.components.Tab.title('Public dashboard').click(); @@ -74,7 +74,7 @@ describe.skip('Public dashboards', () => { e2e.pages.Dashboard.DashNav.publicDashboardTag().should('exist'); // Open sharing modal - e2e.components.NavToolbar.shareDashboard().click(); + e2e.pages.Dashboard.DashNav.shareButton().click(); // Select public dashboards tab cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); @@ -114,7 +114,7 @@ describe.skip('Public dashboards', () => { cy.wait('@query'); // Open sharing modal - e2e.components.NavToolbar.shareDashboard().click(); + e2e.pages.Dashboard.DashNav.shareButton().click(); // Select public dashboards tab cy.intercept('GET', '/api/dashboards/uid/ZqZnVvFZz/public-dashboards').as('query-public-dashboard'); diff --git a/e2e/scenes/dashboards-suite/dashboard-public-templating.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-public-templating.spec.ts similarity index 91% rename from e2e/scenes/dashboards-suite/dashboard-public-templating.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-public-templating.spec.ts index 4a4e660d44e..fa3a4a91242 100644 --- a/e2e/scenes/dashboards-suite/dashboard-public-templating.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-public-templating.spec.ts @@ -10,10 +10,10 @@ describe('Create a public dashboard with template variables shows a template var e2e.flows.openDashboard({ uid: 'HYaGDGIMk' }); // Open sharing modal - e2e.components.NavToolbar.shareDashboard().click(); + e2e.pages.Dashboard.DashNav.shareButton().click(); // Select public dashboards tab - e2e.components.Tab.title('Public Dashboard').click(); + e2e.components.Tab.title('Public dashboard').click(); // Warning Alert dashboard cannot be made public because it has template variables e2e.pages.ShareDashboardModal.PublicDashboard.TemplateVariablesWarningAlert().should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/dashboard-templating.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-templating.spec.ts similarity index 95% rename from e2e/scenes/dashboards-suite/dashboard-templating.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-templating.spec.ts index 4e885df1258..71b9fcbf7db 100644 --- a/e2e/scenes/dashboards-suite/dashboard-templating.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-templating.spec.ts @@ -34,7 +34,7 @@ describe('Dashboard templating', () => { `Server:sqlstring = 'A''A\\"A','BB\\\B','CCC'`, `Server:date = NaN`, `Server:text = All`, - `Server:queryparam = var-Server=A%27A%22A&var-Server=BB%5CB&var-Server=CCC`, + `Server:queryparam = var-Server=All`, `1 < 2`, `Example: from=now-6h&to=now`, ]; diff --git a/e2e/scenes/dashboards-suite/dashboard-time-zone.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-time-zone.spec.ts similarity index 98% rename from e2e/scenes/dashboards-suite/dashboard-time-zone.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-time-zone.spec.ts index 0361e84a236..3c2844188e3 100644 --- a/e2e/scenes/dashboards-suite/dashboard-time-zone.spec.ts +++ b/e2e/old-arch/dashboards-suite/dashboard-time-zone.spec.ts @@ -81,8 +81,7 @@ describe('Dashboard time zone support', () => { } }); - // TODO: remove skip once https://github.com/grafana/grafana/issues/86420 is done - it.skip('Tests relative timezone support and overrides', () => { + it('Tests relative timezone support and overrides', () => { // Open dashboard e2e.flows.openDashboard({ uid: 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8', @@ -124,6 +123,7 @@ describe('Dashboard time zone support', () => { .within(() => { cy.contains('[role="row"]', '00:00:00').should('be.visible'); }); + // Test UTC timezone e2e.flows.setTimeRange({ from: 'now-6h', diff --git a/e2e/scenes/dashboards-suite/dashboard-timepicker.spec.ts b/e2e/old-arch/dashboards-suite/dashboard-timepicker.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/dashboard-timepicker.spec.ts rename to e2e/old-arch/dashboards-suite/dashboard-timepicker.spec.ts diff --git a/e2e/scenes/dashboards-suite/embedded-dashboard.spec.ts b/e2e/old-arch/dashboards-suite/embedded-dashboard.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/embedded-dashboard.spec.ts rename to e2e/old-arch/dashboards-suite/embedded-dashboard.spec.ts diff --git a/e2e/scenes/dashboards-suite/general-dashboards.spec.ts b/e2e/old-arch/dashboards-suite/general-dashboards.spec.ts similarity index 87% rename from e2e/scenes/dashboards-suite/general-dashboards.spec.ts rename to e2e/old-arch/dashboards-suite/general-dashboards.spec.ts index bc1b2a306be..1d01c82267c 100644 --- a/e2e/scenes/dashboards-suite/general-dashboards.spec.ts +++ b/e2e/old-arch/dashboards-suite/general-dashboards.spec.ts @@ -22,9 +22,9 @@ describe('Dashboards', () => { // Then we open and close the panel editor e2e.components.Panels.Panel.menu('Panel #50').click({ force: true }); // it only shows on hover e2e.components.Panels.Panel.menuItems('Edit').click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.components.PanelEditor.applyButton().click(); - // The last panel should still be visible! + // And the last panel should still be visible! e2e.components.Panels.Panel.title('Panel #50').should('be.visible'); }); }); diff --git a/e2e/scenes/dashboards-suite/import-dashboard.spec.ts b/e2e/old-arch/dashboards-suite/import-dashboard.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/import-dashboard.spec.ts rename to e2e/old-arch/dashboards-suite/import-dashboard.spec.ts diff --git a/e2e/scenes/dashboards-suite/load-options-from-url.spec.ts b/e2e/old-arch/dashboards-suite/load-options-from-url.spec.ts similarity index 75% rename from e2e/scenes/dashboards-suite/load-options-from-url.spec.ts rename to e2e/old-arch/dashboards-suite/load-options-from-url.spec.ts index d674ae2a385..c1678a0aa40 100644 --- a/e2e/scenes/dashboards-suite/load-options-from-url.spec.ts +++ b/e2e/old-arch/dashboards-suite/load-options-from-url.spec.ts @@ -16,50 +16,40 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB').should('be.visible'); @@ -75,50 +65,40 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); @@ -145,25 +125,24 @@ describe('Variables - Load options from Url', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('X').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); - cy.get('body').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') - .should('have.length', 2); + .within(() => { + e2e.components.Variables.variableOption().should('have.length', 65); + }); }); }); diff --git a/e2e/scenes/dashboards-suite/new-constant-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-constant-variable.spec.ts similarity index 83% rename from e2e/scenes/dashboards-suite/new-constant-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-constant-variable.spec.ts index 42693acb668..5cb43e21359 100644 --- a/e2e/scenes/dashboards-suite/new-constant-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-constant-variable.spec.ts @@ -9,7 +9,7 @@ describe('Variables - Constant', () => { }); it('can add a new constant variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Constant" variable @@ -22,11 +22,11 @@ describe('Variables - Constant', () => { e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type('pesto').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); - // e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'pesto'); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.components.RefreshPicker.runButtonV2().click(); // Assert it was rendered diff --git a/e2e/scenes/dashboards-suite/new-custom-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-custom-variable.spec.ts similarity index 83% rename from e2e/scenes/dashboards-suite/new-custom-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-custom-variable.spec.ts index bda748fbdf2..7b75622aff7 100644 --- a/e2e/scenes/dashboards-suite/new-custom-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-custom-variable.spec.ts @@ -25,7 +25,7 @@ describe('Variables - Custom', () => { }); it('can add a custom template variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Custom" variable @@ -34,16 +34,17 @@ describe('Variables - Custom', () => { assertPreviewValues(['one', 'two', 'three']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('one').click(); - e2e.components.Select.option().contains('two').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('two').click(); + // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: two'); }); it('can add a custom template variable with labels', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Custom" variable @@ -57,10 +58,10 @@ describe('Variables - Custom', () => { assertPreviewValues(['One', 'Two', 'Three']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('1').click(); - e2e.components.Select.option().contains('Two').click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('One').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('Two').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 2'); diff --git a/e2e/scenes/dashboards-suite/new-datasource-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-datasource-variable.spec.ts similarity index 61% rename from e2e/scenes/dashboards-suite/new-datasource-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-datasource-variable.spec.ts index b56e458567c..40260a76520 100644 --- a/e2e/scenes/dashboards-suite/new-datasource-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-datasource-variable.spec.ts @@ -3,14 +3,16 @@ import { e2e } from '../utils'; const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output'; const DASHBOARD_NAME = 'Test variable output'; -// Skipping due to flakiness/race conditions with same old arch test e2e/dashboards-suite/new-datasource-variable.spec.ts -describe.skip('Variables - Datasource', () => { +const gdev_mysql = 'gdev-mysql'; +const gdev_mysql_ds_tests = 'gdev-mysql-ds-tests'; + +describe('Variables - Datasource', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); it('can add a new datasource variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Datasource" variable @@ -21,30 +23,26 @@ describe.skip('Variables - Datasource', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); - // If this is failing, but sure to check there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus" + // If this is failing, but sure to check there are MySQL datasources named "gdev-mysql" and "gdev-mysql-ds-tests" // Or, just update is to match some gdev datasources to test with :) e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().within(() => { - cy.get('input').type('Prometheus{enter}'); + cy.get('input').type('MySQL{enter}'); }); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('contain.text', gdev_mysql); e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should( 'contain.text', - 'gdev-prometheus' - ); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should( - 'contain.text', - 'gdev-slow-prometheus' + gdev_mysql_ds_tests ); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.components.RefreshPicker.runButtonV2().click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('gdev-prometheus').click(); - e2e.components.Select.option().contains('gdev-slow-prometheus').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(gdev_mysql).click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(gdev_mysql_ds_tests).click(); // Assert it was rendered - cy.get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus-uid'); - cy.get('.markdown-html').should('include.text', 'VariableUnderTestText: gdev-slow-prometheus'); + cy.get('.markdown-html').should('include.text', `VariableUnderTestText: ${gdev_mysql_ds_tests}`); }); }); diff --git a/e2e/scenes/dashboards-suite/new-interval-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-interval-variable.spec.ts similarity index 82% rename from e2e/scenes/dashboards-suite/new-interval-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-interval-variable.spec.ts index 88e1052ce5e..f99c7a2d8b2 100644 --- a/e2e/scenes/dashboards-suite/new-interval-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-interval-variable.spec.ts @@ -16,7 +16,7 @@ describe('Variables - Interval', () => { }); it('can add a new interval variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "Interval" variable @@ -35,13 +35,12 @@ describe('Variables - Interval', () => { assertPreviewValues(['10s', '10m', '60m', '90m', '1h30m']); // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); - + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.components.RefreshPicker.runButtonV2().click(); - e2e.pages.Dashboard.SubMenu.submenuItemLabels('Variable under test').next().should('have.text', `10s`).click(); - e2e.components.Select.option().contains('1h30m').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('10s').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('1h30m').click(); // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: 1h30m'); diff --git a/e2e/scenes/dashboards-suite/new-query-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-query-variable.spec.ts similarity index 80% rename from e2e/scenes/dashboards-suite/new-query-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-query-variable.spec.ts index d6ead2937c4..c5edb437147 100644 --- a/e2e/scenes/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-query-variable.spec.ts @@ -1,5 +1,3 @@ -import { selectors } from '@grafana/e2e-selectors'; - import { e2e } from '../utils'; const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; @@ -11,12 +9,10 @@ describe('Variables - Query - Add variable', () => { }); it('query variable should be default and default fields should be correct', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) - .should('be.visible') - .click(); + e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2() .should('be.visible') @@ -72,17 +68,15 @@ describe('Variables - Query - Add variable', () => { cy.get('input[type="checkbox"]').should('not.be.checked'); }); - e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.have.text'); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist'); e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist'); }); it('adding a single value query variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) - .should('be.visible') - .click(); + e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') @@ -107,27 +101,29 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItem() .should('have.length', 4) .eq(3) .within(() => { - cy.get('input').click(); - }); + e2e.components.Variables.variableLinkWrapper().should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + .should('be.visible') + .within(() => { + e2e.components.Variables.variableOption().should('have.length', 1); + }); - e2e.components.Select.option().should('have.length', 1); - e2e.components.Select.option().contains('C'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); + }); }); it('adding a multi value query variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); - cy.get(`[data-testid="${selectors.pages.Dashboard.Settings.Variables.List.newButton}"]`) - .should('be.visible') - .click(); + e2e.pages.Dashboard.Settings.Variables.List.newButton().should('be.visible').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2() .should('be.visible') @@ -165,21 +161,22 @@ describe('Variables - Query - Add variable', () => { e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('a label').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItem() .should('have.length', 4) .eq(3) .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableLinkWrapper().should('be.visible').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + .should('be.visible') + .within(() => { + e2e.components.Variables.variableOption().should('have.length', 2); + }); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C').should('be.visible'); }); - - e2e.components.Select.option().should('have.length', 3); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - - e2e.components.Select.option().contains('All'); - e2e.components.Select.option().contains('C'); }); }); diff --git a/e2e/scenes/dashboards-suite/new-text-box-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-text-box-variable.spec.ts similarity index 75% rename from e2e/scenes/dashboards-suite/new-text-box-variable.spec.ts rename to e2e/old-arch/dashboards-suite/new-text-box-variable.spec.ts index 7da9afa32bf..b6cbabceef8 100644 --- a/e2e/scenes/dashboards-suite/new-text-box-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-text-box-variable.spec.ts @@ -9,25 +9,26 @@ describe('Variables - Text box', () => { }); it('can add a new text box variable', () => { - e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=variables` }); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&editview=templating` }); cy.contains(DASHBOARD_NAME).should('be.visible'); // Create a new "text box" variable e2e.components.CallToActionCard.buttonV2('Add variable').click(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => { - cy.get('input').type('Textbox{enter}'); + cy.get('input').type('Text box{enter}'); }); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur(); e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2().type('cat-dog').blur(); + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().eq(0).should('have.text', 'cat-dog'); + // Navigate back to the homepage and change the selected variable value - e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); - e2e.pages.Dashboard.SubMenu.submenuItem().within(() => { - cy.get('input').clear().type('dog-cat').blur(); - }); + e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); + cy.get('#var-VariableUnderTest').clear().type('dog-cat').blur(); + // Assert it was rendered cy.get('.markdown-html').should('include.text', 'VariableUnderTest: dog-cat'); }); diff --git a/e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts b/e2e/old-arch/dashboards-suite/set-options-from-ui.spec.ts similarity index 73% rename from e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts rename to e2e/old-arch/dashboards-suite/set-options-from-ui.spec.ts index bdcc5c6b516..d7ec950487b 100644 --- a/e2e/scenes/dashboards-suite/set-options-from-ui.spec.ts +++ b/e2e/old-arch/dashboards-suite/set-options-from-ui.spec.ts @@ -1,5 +1,3 @@ -import { selectors } from '@grafana/e2e-selectors'; - import { e2e } from '../utils'; const PAGE_UNDER_TEST = '-Y-tnEDWk/templating-nested-template-variables'; @@ -12,46 +10,39 @@ describe('Variables - Set options from ui', () => { it('clicking a value that is not part of dependents options should change these to All', () => { e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?orgId=1&var-datacenter=A&var-server=AA&var-pod=AAA` }); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') - .should('be.visible') - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); - cy.get('body').click(); + e2e.components.NavToolbar.container().click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All') .should('have.length', 2) .eq(0) + .should('be.visible') + .click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().parent().should('have.length', 10); - - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (1)'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - cy.get('body').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemLabels('pod') - .parent() + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() + .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 65); }); - // length is 11 because of virtualized select options - e2e.components.Select.option().parent().should('have.length', 11); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BAB').should('be.visible'); @@ -69,47 +60,41 @@ describe('Variables - Set options from ui', () => { cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A') - .should('be.visible') - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').should('be.visible').click(); - e2e.components.Select.toggleAllOptions().should('have.text', 'Selected (2)'); - - cy.get('body').click(); + e2e.components.NavToolbar.container().click(); cy.wait('@query'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B').scrollIntoView().should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').scrollIntoView().should('be.visible'); - cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); + e2e.components.LoadingIndicator.icon().should('have.length', 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 17); }); - e2e.components.Select.option().should('have.length', 11); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AC').should('be.visible'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AD').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - cy.get('body').click(); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().should('have.length', 10); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAB').should('be.visible'); @@ -123,46 +108,38 @@ describe('Variables - Set options from ui', () => { cy.intercept({ pathname: '/api/ds/query' }).as('query'); cy.wait('@query'); - cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A,B') - .should('be.visible') - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A + B').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A').should('be.visible').click(); - cy.get('body').click(); + e2e.components.NavToolbar.container().click(); cy.wait('@query'); - cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B').scrollIntoView().should('be.visible'); - cy.get(`[aria-label="${selectors.components.LoadingIndicator.icon}"]`).should('not.exist'); + e2e.components.LoadingIndicator.icon().should('have.length', 0); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB').should('be.visible').click(); + + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().should('have.length', 10); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('All').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible'); - cy.get('body').click(0, 0); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB').should('be.visible').click(); - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB') + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown() .should('be.visible') .within(() => { - cy.get('input').click(); + e2e.components.Variables.variableOption().should('have.length', 9); }); - e2e.components.Select.option().should('have.length', 10); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible'); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible'); diff --git a/e2e/scenes/dashboards-suite/templating-dashboard-links-and-variables.spec.ts b/e2e/old-arch/dashboards-suite/templating-dashboard-links-and-variables.spec.ts similarity index 85% rename from e2e/scenes/dashboards-suite/templating-dashboard-links-and-variables.spec.ts rename to e2e/old-arch/dashboards-suite/templating-dashboard-links-and-variables.spec.ts index 829303aab41..06e13002db4 100644 --- a/e2e/scenes/dashboards-suite/templating-dashboard-links-and-variables.spec.ts +++ b/e2e/old-arch/dashboards-suite/templating-dashboard-links-and-variables.spec.ts @@ -27,27 +27,21 @@ describe('Templating', () => { expect(links).to.have.length.greaterThan(13); for (let index = 0; index < links.length; index++) { - expect(Cypress.$(links[index]).attr('href')).contains(variableValue); + expect(Cypress.$(links[index]).attr('href')).contains(`var-custom=${variableValue}`); } }); }; e2e.components.DashboardLinks.dropDown().should('be.visible').click().wait('@tagsTemplatingSearch'); - verifyLinks('var-custom=p1&var-custom=p2&var-custom=p3'); + // verify all links, should have All value + verifyLinks('All'); - cy.get('body').click(); - - e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('$__all') - .should('be.visible') - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2').should('be.visible').click(); - cy.get('body').click(); - + e2e.components.NavToolbar.container().click(); e2e.components.DashboardLinks.dropDown() .scrollIntoView() .should('be.visible') diff --git a/e2e/scenes/dashboards-suite/textbox-variables.spec.ts b/e2e/old-arch/dashboards-suite/textbox-variables.spec.ts similarity index 100% rename from e2e/scenes/dashboards-suite/textbox-variables.spec.ts rename to e2e/old-arch/dashboards-suite/textbox-variables.spec.ts diff --git a/e2e/scenes/dashboards-suite/utils/makeDashboard.ts b/e2e/old-arch/dashboards-suite/utils/makeDashboard.ts similarity index 100% rename from e2e/scenes/dashboards-suite/utils/makeDashboard.ts rename to e2e/old-arch/dashboards-suite/utils/makeDashboard.ts diff --git a/e2e/scenes/dashboards/DashboardLiveTest.json b/e2e/old-arch/dashboards/DashboardLiveTest.json similarity index 100% rename from e2e/scenes/dashboards/DashboardLiveTest.json rename to e2e/old-arch/dashboards/DashboardLiveTest.json diff --git a/e2e/scenes/dashboards/DashboardSearchTest.json b/e2e/old-arch/dashboards/DashboardSearchTest.json similarity index 100% rename from e2e/scenes/dashboards/DashboardSearchTest.json rename to e2e/old-arch/dashboards/DashboardSearchTest.json diff --git a/e2e/scenes/dashboards/PanelSandboxDashboard.json b/e2e/old-arch/dashboards/PanelSandboxDashboard.json similarity index 100% rename from e2e/scenes/dashboards/PanelSandboxDashboard.json rename to e2e/old-arch/dashboards/PanelSandboxDashboard.json diff --git a/e2e/scenes/dashboards/TestDashboard.json b/e2e/old-arch/dashboards/TestDashboard.json similarity index 100% rename from e2e/scenes/dashboards/TestDashboard.json rename to e2e/old-arch/dashboards/TestDashboard.json diff --git a/e2e/scenes/panels-suite/dashlist.spec.ts b/e2e/old-arch/panels-suite/dashlist.spec.ts similarity index 88% rename from e2e/scenes/panels-suite/dashlist.spec.ts rename to e2e/old-arch/panels-suite/dashlist.spec.ts index 7ad25207910..ec053d3e5e4 100644 --- a/e2e/scenes/panels-suite/dashlist.spec.ts +++ b/e2e/old-arch/panels-suite/dashlist.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const PAGE_UNDER_TEST = 'a6801696-cc53-4196-b1f9-2403e3909185/panel-tests-dashlist-variables'; describe('DashList panel', () => { @@ -20,11 +20,7 @@ describe('DashList panel', () => { }); // update variable to b - e2e.pages.Dashboard.SubMenu.submenuItemLabels('server') - .parent() - .within(() => { - cy.get('input').click(); - }); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('server').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click(); // blur the dropdown cy.get('body').click(); diff --git a/e2e/scenes/panels-suite/datagrid-data-change.spec.ts b/e2e/old-arch/panels-suite/datagrid-data-change.spec.ts similarity index 97% rename from e2e/scenes/panels-suite/datagrid-data-change.spec.ts rename to e2e/old-arch/panels-suite/datagrid-data-change.spec.ts index e7950030659..583f30e267f 100644 --- a/e2e/scenes/panels-suite/datagrid-data-change.spec.ts +++ b/e2e/old-arch/panels-suite/datagrid-data-change.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; const DATAGRID_SELECT_SERIES = 'Datagrid Select series'; diff --git a/e2e/scenes/panels-suite/datagrid-editing-features.spec.ts b/e2e/old-arch/panels-suite/datagrid-editing-features.spec.ts similarity index 99% rename from e2e/scenes/panels-suite/datagrid-editing-features.spec.ts rename to e2e/old-arch/panels-suite/datagrid-editing-features.spec.ts index 71e69b21022..e3592f60388 100644 --- a/e2e/scenes/panels-suite/datagrid-editing-features.spec.ts +++ b/e2e/old-arch/panels-suite/datagrid-editing-features.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'c01bf42b-b783-4447-a304-8554cee1843b'; const DATAGRID_CANVAS = 'data-grid-canvas'; diff --git a/e2e/scenes/panels-suite/frontend-sandbox-panel.spec.ts b/e2e/old-arch/panels-suite/frontend-sandbox-panel.spec.ts similarity index 93% rename from e2e/scenes/panels-suite/frontend-sandbox-panel.spec.ts rename to e2e/old-arch/panels-suite/frontend-sandbox-panel.spec.ts index 582d6ba6497..bab59b99866 100644 --- a/e2e/scenes/panels-suite/frontend-sandbox-panel.spec.ts +++ b/e2e/old-arch/panels-suite/frontend-sandbox-panel.spec.ts @@ -1,9 +1,9 @@ -import panelSandboxDashboard from '../../dashboards/PanelSandboxDashboard.json'; -import { e2e } from '../../utils'; +import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; -// Skipping due to race conditions with same old arch test e2e/panels-suite/frontend-sandbox-panel.spec.ts -describe.skip('Panel sandbox', () => { + +describe('Panel sandbox', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true); diff --git a/e2e/scenes/panels-suite/geomap-layer-types.spec.ts b/e2e/old-arch/panels-suite/geomap-layer-types.spec.ts similarity index 91% rename from e2e/scenes/panels-suite/geomap-layer-types.spec.ts rename to e2e/old-arch/panels-suite/geomap-layer-types.spec.ts index 2e60e88f3f5..17779dde1c9 100644 --- a/e2e/scenes/panels-suite/geomap-layer-types.spec.ts +++ b/e2e/old-arch/panels-suite/geomap-layer-types.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'P2jR04WVk'; @@ -21,41 +21,41 @@ describe('Geomap layer types', () => { e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Heatmap{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('heatmap'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // GeoJSON e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('GeoJSON{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('geojson'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // Open Street Map e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Open Street Map{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('osm-standard'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('not.exist'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // CARTO basemap e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('CARTO basemap{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('carto'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // ArcGIS MapServer e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('ArcGIS MapServer{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('esri-xyz'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); // XYZ Tile layer e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('XYZ Tile layer{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('xyz'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution').should('be.visible'); - // e2e.components.PanelEditor.General.content().should('be.visible'); + e2e.components.PanelEditor.General.content().should('be.visible'); }); it.skip('Tests changing the layer type (alpha)', () => { diff --git a/e2e/scenes/panels-suite/geomap-map-controls.spec.ts b/e2e/old-arch/panels-suite/geomap-map-controls.spec.ts similarity index 97% rename from e2e/scenes/panels-suite/geomap-map-controls.spec.ts rename to e2e/old-arch/panels-suite/geomap-map-controls.spec.ts index 21ba2f73c8b..fbbc1c2c516 100644 --- a/e2e/scenes/panels-suite/geomap-map-controls.spec.ts +++ b/e2e/old-arch/panels-suite/geomap-map-controls.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'P2jR04WVk'; describe('Geomap layer controls options', () => { diff --git a/e2e/scenes/panels-suite/geomap-spatial-operations-transform.spec.ts b/e2e/old-arch/panels-suite/geomap-spatial-operations-transform.spec.ts similarity index 93% rename from e2e/scenes/panels-suite/geomap-spatial-operations-transform.spec.ts rename to e2e/old-arch/panels-suite/geomap-spatial-operations-transform.spec.ts index 66fb1f102e0..52ca7ed0fac 100644 --- a/e2e/scenes/panels-suite/geomap-spatial-operations-transform.spec.ts +++ b/e2e/old-arch/panels-suite/geomap-spatial-operations-transform.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const DASHBOARD_ID = 'P2jR04WVk'; @@ -9,7 +9,7 @@ describe('Geomap spatial operations', () => { it('Tests location auto option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -27,7 +27,7 @@ describe('Geomap spatial operations', () => { it('Tests location coords option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -51,7 +51,7 @@ describe('Geomap spatial operations', () => { it('Tests geoshash field column appears in table view', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -74,7 +74,7 @@ describe('Geomap spatial operations', () => { it('Tests location lookup option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); diff --git a/e2e/scenes/panels-suite/panelEdit_base.spec.ts b/e2e/old-arch/panels-suite/panelEdit_base.spec.ts similarity index 75% rename from e2e/scenes/panels-suite/panelEdit_base.spec.ts rename to e2e/old-arch/panels-suite/panelEdit_base.spec.ts index e8d0e301aff..744c41ed8ce 100644 --- a/e2e/scenes/panels-suite/panelEdit_base.spec.ts +++ b/e2e/old-arch/panels-suite/panelEdit_base.spec.ts @@ -1,6 +1,4 @@ -import { selectors } from '@grafana/e2e-selectors'; - -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const PANEL_UNDER_TEST = 'Lines 500 data points'; @@ -18,53 +16,62 @@ describe('Panel edit tests', () => { e2e.flows.openPanelMenuItem(e2e.flows.PanelMenuItems.Edit, PANEL_UNDER_TEST); - // // New panel editor opens when navigating from Panel menu + // New panel editor opens when navigating from Panel menu e2e.components.PanelEditor.General.content().should('be.visible'); // Queries tab is rendered and open by default e2e.components.PanelEditor.DataPane.content() - .scrollIntoView() .should('be.visible') .within(() => { - e2e.components.Tab.title('Queries').should('be.visible'); + e2e.components.Tab.title('Query').should('be.visible'); // data should be the active tab e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Queries1'); // there's already a query so therefore Query + 1 + expect(li.text()).equals('Query1'); // there's already a query so therefore Query + 1 }); - // cy.get('[data-testid]="query-editor-rows"').should('be.visible'); - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('be.visible'); + e2e.components.QueryTab.content().should('be.visible'); e2e.components.TransformTab.content().should('not.exist'); e2e.components.AlertTab.content().should('not.exist'); e2e.components.PanelAlertTabContent.content().should('not.exist'); // Bottom pane tabs // Can change to Transform tab - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Transformations0'); // there's no transform so therefore Transform + 0 + expect(li.text()).equals('Transform data0'); // there's no transform so therefore Transform + 0 }); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible'); - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); + e2e.components.QueryTab.content().should('not.exist'); e2e.components.AlertTab.content().should('not.exist'); e2e.components.PanelAlertTabContent.content().should('not.exist'); // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) - e2e.components.Tab.title('Alert').scrollIntoView().should('be.visible').click(); + e2e.components.Tab.title('Alert').should('be.visible').click(); e2e.components.Tab.active().should('have.text', 'Alert0'); // there's no alert so therefore Alert + 0 // Needs to be disabled until Grafana EE turns unified alerting on by default // e2e.components.AlertTab.content().should('not.exist'); - cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); + e2e.components.QueryTab.content().should('not.exist'); e2e.components.TransformTab.content().should('not.exist'); // Needs to be disabled until Grafana EE turns unified alerting on by default // e2e.components.PanelAlertTabContent.content().should('exist'); // e2e.components.PanelAlertTabContent.content().should('be.visible'); - e2e.components.Tab.title('Queries').should('be.visible').click(); + e2e.components.Tab.title('Query').should('be.visible').click(); }); + // Panel sidebar is rendered open by default + e2e.components.PanelEditor.OptionsPane.content().should('be.visible'); + + // close options pane + e2e.components.PanelEditor.toggleVizOptions().click(); + e2e.components.PanelEditor.OptionsPane.content().should('not.exist'); + + // open options pane + e2e.components.PanelEditor.toggleVizOptions().should('be.visible').click(); + e2e.components.PanelEditor.OptionsPane.content().should('be.visible'); + // Check that Time series is chosen e2e.components.PanelEditor.toggleVizPicker().click(); e2e.components.PluginVisualization.item('Time series').should('be.visible'); @@ -95,8 +102,6 @@ describe('Panel edit tests', () => { e2e.components.PanelEditor.DataPane.content().should('be.visible'); // Field & Overrides tabs (need to switch to React based vis, i.e. Table) - e2e.components.PanelEditor.toggleTableView().click({ force: true }).click({ force: true }); - e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible'); }); diff --git a/e2e/scenes/panels-suite/panelEdit_queries.spec.ts b/e2e/old-arch/panels-suite/panelEdit_queries.spec.ts similarity index 99% rename from e2e/scenes/panels-suite/panelEdit_queries.spec.ts rename to e2e/old-arch/panels-suite/panelEdit_queries.spec.ts index a06b05a1668..1700a00ea9f 100644 --- a/e2e/scenes/panels-suite/panelEdit_queries.spec.ts +++ b/e2e/old-arch/panels-suite/panelEdit_queries.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; const flakyTimeout = 10000; diff --git a/e2e/scenes/panels-suite/panelEdit_transforms.spec.ts b/e2e/old-arch/panels-suite/panelEdit_transforms.spec.ts similarity index 86% rename from e2e/scenes/panels-suite/panelEdit_transforms.spec.ts rename to e2e/old-arch/panels-suite/panelEdit_transforms.spec.ts index ef14d6e55a5..eb3029b8d98 100644 --- a/e2e/scenes/panels-suite/panelEdit_transforms.spec.ts +++ b/e2e/old-arch/panels-suite/panelEdit_transforms.spec.ts @@ -1,4 +1,4 @@ -import { e2e } from '../../utils'; +import { e2e } from '../utils'; describe('Panel edit tests - transformations', () => { beforeEach(() => { @@ -8,7 +8,7 @@ describe('Panel edit tests - transformations', () => { it('Tests transformations editor', () => { e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); e2e.components.Transforms.Reduce.calculationsLabel().scrollIntoView().should('be.visible'); @@ -18,7 +18,7 @@ describe('Panel edit tests - transformations', () => { it('Tests case where transformations can be disabled and not clear out panel data', () => { e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - e2e.components.Tab.title('Transformations').should('be.visible').click(); + e2e.components.Tab.title('Transform data').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); e2e.components.Transforms.disableTransformationButton().should('be.visible').click(); diff --git a/e2e/scenes/shared/smokeTestScenario.ts b/e2e/old-arch/shared/smokeTestScenario.ts similarity index 70% rename from e2e/scenes/shared/smokeTestScenario.ts rename to e2e/old-arch/shared/smokeTestScenario.ts index 3e66419680f..537bd0bb23c 100644 --- a/e2e/scenes/shared/smokeTestScenario.ts +++ b/e2e/old-arch/shared/smokeTestScenario.ts @@ -3,17 +3,12 @@ import { e2e } from '../utils'; export const smokeTestScenario = () => describe('Smoke tests', () => { before(() => { - e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false); - cy.logToConsole('enabling dashboardScene feature toggle in localstorage'); - cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=true'); + cy.logToConsole('disabling dashboardScene feature toggle in localstorage'); + cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=false'); cy.reload(); + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false); e2e.flows.addDataSource(); e2e.flows.addDashboard(); - e2e.flows.addPanel({ - dataSourceName: 'gdev-testdata', - visitDashboardAtStart: false, - timeout: 10000, - }); }); after(() => { @@ -21,6 +16,11 @@ export const smokeTestScenario = () => }); it('Login scenario, create test data source, dashboard, panel, and export scenario', () => { + // wait for time to be set to account for any layout shift + cy.contains('2020-01-01 00:00:00 to 2020-01-01 06:00:00').should('be.visible'); + e2e.components.PageToolbar.itemButton('Add button').click(); + e2e.components.PageToolbar.itemButton('Add new visualization menu item').click(); + e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer() .should('be.visible') .within(() => { @@ -32,7 +32,8 @@ export const smokeTestScenario = () => // Make sure the graph renders via checking legend e2e.components.VizLegend.seriesName('A-series').should('be.visible'); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); + // Expand options section + e2e.components.PanelEditor.applyButton(); // Make sure panel is & visualization is added to dashboard e2e.components.VizLegend.seriesName('A-series').should('be.visible'); diff --git a/e2e/scenes/smoke-tests-suite/1-smoketests.spec.ts b/e2e/old-arch/smoke-tests-suite/1-smoketests.spec.ts similarity index 100% rename from e2e/scenes/smoke-tests-suite/1-smoketests.spec.ts rename to e2e/old-arch/smoke-tests-suite/1-smoketests.spec.ts diff --git a/e2e/scenes/smoke-tests-suite/panels_smokescreen.spec.ts b/e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts similarity index 66% rename from e2e/scenes/smoke-tests-suite/panels_smokescreen.spec.ts rename to e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts index ca63dadba19..5ba784d72b2 100644 --- a/e2e/scenes/smoke-tests-suite/panels_smokescreen.spec.ts +++ b/e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts @@ -14,11 +14,17 @@ describe('Panels smokescreen', () => { it('Tests each panel type in the panel edit view to ensure no crash', () => { e2e.flows.addDashboard(); - e2e.flows.addPanel({ - dataSourceName: 'gdev-testdata', - timeout: 10000, - visitDashboardAtStart: false, - }); + // TODO: Try and use e2e.flows.addPanel() instead of block below + try { + e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); + e2e.components.PageToolbar.itemButton('Add button').click(); + } catch (e) { + // Depending on the screen size, the "Add panel" button might be hidden + e2e.components.PageToolbar.item('Show more items').click(); + e2e.components.PageToolbar.item('Add button').last().click(); + } + e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); + e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); cy.window().then((win: Cypress.AUTWindow & { grafanaBootData: GrafanaBootConfig['bootData'] }) => { // Loop through every panel type and ensure no crash diff --git a/e2e/scenes/utils/flows/addDashboard.ts b/e2e/old-arch/utils/flows/addDashboard.ts similarity index 97% rename from e2e/scenes/utils/flows/addDashboard.ts rename to e2e/old-arch/utils/flows/addDashboard.ts index 77dd07e9c9b..cc8e7548032 100644 --- a/e2e/scenes/utils/flows/addDashboard.ts +++ b/e2e/old-arch/utils/flows/addDashboard.ts @@ -139,9 +139,9 @@ export const addDashboard = (config?: Partial) => { setDashboardTimeRange(timeRange); - e2e.components.NavToolbar.editDashboard.saveButton().click(); - e2e.components.Drawer.DashboardSaveDrawer.saveAsTitleInput().clear().type(title, { force: true }); - e2e.components.Drawer.DashboardSaveDrawer.saveButton().click(); + e2e.components.PageToolbar.item('Save dashboard').click(); + e2e.pages.SaveDashboardAsModal.newName().clear().type(title, { force: true }); + e2e.pages.SaveDashboardAsModal.save().click(); e2e.flows.assertSuccessNotification(); e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible'); diff --git a/e2e/scenes/utils/flows/addDataSource.ts b/e2e/old-arch/utils/flows/addDataSource.ts similarity index 100% rename from e2e/scenes/utils/flows/addDataSource.ts rename to e2e/old-arch/utils/flows/addDataSource.ts diff --git a/e2e/scenes/utils/flows/addPanel.ts b/e2e/old-arch/utils/flows/addPanel.ts similarity index 100% rename from e2e/scenes/utils/flows/addPanel.ts rename to e2e/old-arch/utils/flows/addPanel.ts diff --git a/e2e/scenes/utils/flows/assertSuccessNotification.ts b/e2e/old-arch/utils/flows/assertSuccessNotification.ts similarity index 100% rename from e2e/scenes/utils/flows/assertSuccessNotification.ts rename to e2e/old-arch/utils/flows/assertSuccessNotification.ts diff --git a/e2e/scenes/utils/flows/configurePanel.ts b/e2e/old-arch/utils/flows/configurePanel.ts similarity index 93% rename from e2e/scenes/utils/flows/configurePanel.ts rename to e2e/old-arch/utils/flows/configurePanel.ts index 7a881a47c17..4aa6a6cc5ab 100644 --- a/e2e/scenes/utils/flows/configurePanel.ts +++ b/e2e/old-arch/utils/flows/configurePanel.ts @@ -85,17 +85,15 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC e2e.components.Panels.Panel.headerItems('Edit').click(); } else { try { - //Enter edit mode - e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); - e2e.components.PageToolbar.itemButton('Add button').should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.addVisualizationButton().should('be.visible').click(); + e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); + e2e.components.PageToolbar.itemButton('Add button').click(); } catch (e) { // Depending on the screen size, the "Add" button might be hidden e2e.components.PageToolbar.item('Show more items').click(); e2e.components.PageToolbar.item('Add button').last().click(); } - // e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); - // e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); + e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); + e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); } if (timeRange) { diff --git a/e2e/scenes/utils/flows/confirmModal.ts b/e2e/old-arch/utils/flows/confirmModal.ts similarity index 100% rename from e2e/scenes/utils/flows/confirmModal.ts rename to e2e/old-arch/utils/flows/confirmModal.ts diff --git a/e2e/scenes/utils/flows/deleteDashboard.ts b/e2e/old-arch/utils/flows/deleteDashboard.ts similarity index 100% rename from e2e/scenes/utils/flows/deleteDashboard.ts rename to e2e/old-arch/utils/flows/deleteDashboard.ts diff --git a/e2e/scenes/utils/flows/deleteDataSource.ts b/e2e/old-arch/utils/flows/deleteDataSource.ts similarity index 100% rename from e2e/scenes/utils/flows/deleteDataSource.ts rename to e2e/old-arch/utils/flows/deleteDataSource.ts diff --git a/e2e/scenes/utils/flows/editPanel.ts b/e2e/old-arch/utils/flows/editPanel.ts similarity index 100% rename from e2e/scenes/utils/flows/editPanel.ts rename to e2e/old-arch/utils/flows/editPanel.ts diff --git a/e2e/scenes/utils/flows/importDashboard.ts b/e2e/old-arch/utils/flows/importDashboard.ts similarity index 95% rename from e2e/scenes/utils/flows/importDashboard.ts rename to e2e/old-arch/utils/flows/importDashboard.ts index d3c6b7e23b5..4e7b8ade4ad 100644 --- a/e2e/scenes/utils/flows/importDashboard.ts +++ b/e2e/old-arch/utils/flows/importDashboard.ts @@ -51,9 +51,7 @@ export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: num e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click(); e2e.components.Tab.title('JSON').should('be.visible').click(); - e2e.components.PanelInspector.Json.content().should('be.visible'); - e2e.components.ReactMonacoEditor.editorLazy().should('be.visible'); - cy.contains('Panel JSON').click({ force: true }); + e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click({ force: true }); e2e.components.Select.option().should('be.visible').contains('Panel data').click(); // ensures that panel has loaded without knowingly hitting an error diff --git a/e2e/scenes/utils/flows/importDashboards.ts b/e2e/old-arch/utils/flows/importDashboards.ts similarity index 100% rename from e2e/scenes/utils/flows/importDashboards.ts rename to e2e/old-arch/utils/flows/importDashboards.ts diff --git a/e2e/scenes/utils/flows/index.ts b/e2e/old-arch/utils/flows/index.ts similarity index 100% rename from e2e/scenes/utils/flows/index.ts rename to e2e/old-arch/utils/flows/index.ts diff --git a/e2e/scenes/utils/flows/login.ts b/e2e/old-arch/utils/flows/login.ts similarity index 100% rename from e2e/scenes/utils/flows/login.ts rename to e2e/old-arch/utils/flows/login.ts diff --git a/e2e/scenes/utils/flows/openDashboard.ts b/e2e/old-arch/utils/flows/openDashboard.ts similarity index 100% rename from e2e/scenes/utils/flows/openDashboard.ts rename to e2e/old-arch/utils/flows/openDashboard.ts diff --git a/e2e/scenes/utils/flows/openPanelMenuItem.ts b/e2e/old-arch/utils/flows/openPanelMenuItem.ts similarity index 100% rename from e2e/scenes/utils/flows/openPanelMenuItem.ts rename to e2e/old-arch/utils/flows/openPanelMenuItem.ts diff --git a/e2e/scenes/utils/flows/revertAllChanges.ts b/e2e/old-arch/utils/flows/revertAllChanges.ts similarity index 100% rename from e2e/scenes/utils/flows/revertAllChanges.ts rename to e2e/old-arch/utils/flows/revertAllChanges.ts diff --git a/e2e/scenes/utils/flows/saveDashboard.ts b/e2e/old-arch/utils/flows/saveDashboard.ts similarity index 100% rename from e2e/scenes/utils/flows/saveDashboard.ts rename to e2e/old-arch/utils/flows/saveDashboard.ts diff --git a/e2e/scenes/utils/flows/selectOption.ts b/e2e/old-arch/utils/flows/selectOption.ts similarity index 100% rename from e2e/scenes/utils/flows/selectOption.ts rename to e2e/old-arch/utils/flows/selectOption.ts diff --git a/e2e/scenes/utils/flows/setDashboardTimeRange.ts b/e2e/old-arch/utils/flows/setDashboardTimeRange.ts similarity index 100% rename from e2e/scenes/utils/flows/setDashboardTimeRange.ts rename to e2e/old-arch/utils/flows/setDashboardTimeRange.ts diff --git a/e2e/scenes/utils/flows/setTimeRange.ts b/e2e/old-arch/utils/flows/setTimeRange.ts similarity index 100% rename from e2e/scenes/utils/flows/setTimeRange.ts rename to e2e/old-arch/utils/flows/setTimeRange.ts diff --git a/e2e/scenes/utils/flows/userPreferences.ts b/e2e/old-arch/utils/flows/userPreferences.ts similarity index 100% rename from e2e/scenes/utils/flows/userPreferences.ts rename to e2e/old-arch/utils/flows/userPreferences.ts diff --git a/e2e/scenes/utils/index.ts b/e2e/old-arch/utils/index.ts similarity index 100% rename from e2e/scenes/utils/index.ts rename to e2e/old-arch/utils/index.ts diff --git a/e2e/scenes/utils/support/benchmark.ts b/e2e/old-arch/utils/support/benchmark.ts similarity index 100% rename from e2e/scenes/utils/support/benchmark.ts rename to e2e/old-arch/utils/support/benchmark.ts diff --git a/e2e/old-arch/utils/support/clipboard.ts b/e2e/old-arch/utils/support/clipboard.ts new file mode 100644 index 00000000000..aa5841ea2d6 --- /dev/null +++ b/e2e/old-arch/utils/support/clipboard.ts @@ -0,0 +1,29 @@ +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + copyToClipboard(): Chainable; + copyFromClipboard(): Chainable; + } + } +} + +Cypress.Commands.add('copyFromClipboard', () => { + return cy.window().then((win) => { + return cy.wrap(win.navigator.clipboard.readText()); + }); +}); + +Cypress.Commands.add( + 'copyToClipboard', + { + prevSubject: [], + }, + (subject: string) => { + return cy.window().then((win) => { + return cy.wrap(win.navigator.clipboard.writeText(subject)); + }); + } +); + +export {}; diff --git a/e2e/scenes/utils/support/index.ts b/e2e/old-arch/utils/support/index.ts similarity index 100% rename from e2e/scenes/utils/support/index.ts rename to e2e/old-arch/utils/support/index.ts diff --git a/e2e/scenes/utils/support/localStorage.ts b/e2e/old-arch/utils/support/localStorage.ts similarity index 100% rename from e2e/scenes/utils/support/localStorage.ts rename to e2e/old-arch/utils/support/localStorage.ts diff --git a/e2e/scenes/utils/support/monaco.ts b/e2e/old-arch/utils/support/monaco.ts similarity index 100% rename from e2e/scenes/utils/support/monaco.ts rename to e2e/old-arch/utils/support/monaco.ts diff --git a/e2e/scenes/utils/support/scenarioContext.ts b/e2e/old-arch/utils/support/scenarioContext.ts similarity index 100% rename from e2e/scenes/utils/support/scenarioContext.ts rename to e2e/old-arch/utils/support/scenarioContext.ts diff --git a/e2e/scenes/utils/support/selector.ts b/e2e/old-arch/utils/support/selector.ts similarity index 100% rename from e2e/scenes/utils/support/selector.ts rename to e2e/old-arch/utils/support/selector.ts diff --git a/e2e/scenes/utils/support/types.ts b/e2e/old-arch/utils/support/types.ts similarity index 100% rename from e2e/scenes/utils/support/types.ts rename to e2e/old-arch/utils/support/types.ts diff --git a/e2e/scenes/utils/support/url.ts b/e2e/old-arch/utils/support/url.ts similarity index 92% rename from e2e/scenes/utils/support/url.ts rename to e2e/old-arch/utils/support/url.ts index 34620974b06..94e42827940 100644 --- a/e2e/scenes/utils/support/url.ts +++ b/e2e/old-arch/utils/support/url.ts @@ -1,5 +1,3 @@ -import { e2e } from '../index'; - const getBaseUrl = () => Cypress.env('BASE_URL') || Cypress.config().baseUrl || 'http://localhost:3000'; export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href; diff --git a/e2e/scenes/utils/typings/index.ts b/e2e/old-arch/utils/typings/index.ts similarity index 100% rename from e2e/scenes/utils/typings/index.ts rename to e2e/old-arch/utils/typings/index.ts diff --git a/e2e/scenes/utils/typings/undo.ts b/e2e/old-arch/utils/typings/undo.ts similarity index 100% rename from e2e/scenes/utils/typings/undo.ts rename to e2e/old-arch/utils/typings/undo.ts diff --git a/e2e/scenes/various-suite/bar-gauge.spec.ts b/e2e/old-arch/various-suite/bar-gauge.spec.ts similarity index 76% rename from e2e/scenes/various-suite/bar-gauge.spec.ts rename to e2e/old-arch/various-suite/bar-gauge.spec.ts index 7c28c48a146..5f8506b8050 100644 --- a/e2e/scenes/various-suite/bar-gauge.spec.ts +++ b/e2e/old-arch/various-suite/bar-gauge.spec.ts @@ -11,9 +11,7 @@ describe('Bar Gauge Panel', () => { // open Panel Tests - Bar Gauge e2e.flows.openDashboard({ uid: 'O6f11TZWk' }); - cy.get( - `[data-viz-panel-key="panel-6"] [data-testid^="${selectors.components.Panels.Visualization.BarGauge.valueV2}"]` - ) + cy.get(`[data-panelid=6] [data-testid^="${selectors.components.Panels.Visualization.BarGauge.valueV2}"]`) .should('have.css', 'color', 'rgb(242, 73, 92)') .contains('100'); }); diff --git a/e2e/scenes/various-suite/exemplars.spec.ts b/e2e/old-arch/various-suite/exemplars.spec.ts similarity index 93% rename from e2e/scenes/various-suite/exemplars.spec.ts rename to e2e/old-arch/various-suite/exemplars.spec.ts index 675b83039ba..b3c422c1294 100644 --- a/e2e/scenes/various-suite/exemplars.spec.ts +++ b/e2e/old-arch/various-suite/exemplars.spec.ts @@ -17,8 +17,8 @@ const addDataSource = () => { }, }); }; -// Skipping due to race conditions with same old arch test e2e/various-suite/exemplars.spec.ts -describe.skip('Exemplars', () => { + +describe('Exemplars', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); @@ -69,7 +69,7 @@ describe.skip('Exemplars', () => { cy.get(`[data-testid="time-series-zoom-to-data"]`).click(); - e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove', { force: true }); + e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove'); cy.contains('Query with gdev-tempo').click(); e2e.components.TraceViewer.spanBar().should('have.length', 11); }); diff --git a/e2e/scenes/various-suite/explore.spec.ts b/e2e/old-arch/various-suite/explore.spec.ts similarity index 100% rename from e2e/scenes/various-suite/explore.spec.ts rename to e2e/old-arch/various-suite/explore.spec.ts diff --git a/e2e/scenes/various-suite/filter-annotations.spec.ts b/e2e/old-arch/various-suite/filter-annotations.spec.ts similarity index 61% rename from e2e/scenes/various-suite/filter-annotations.spec.ts rename to e2e/old-arch/various-suite/filter-annotations.spec.ts index 35319ee1beb..26cf83f00e2 100644 --- a/e2e/scenes/various-suite/filter-annotations.spec.ts +++ b/e2e/old-arch/various-suite/filter-annotations.spec.ts @@ -1,8 +1,7 @@ import { e2e } from '../utils'; const DASHBOARD_ID = 'ed155665'; -// Skipping due to race conditions with same old arch test e2e/various-suite/filter-annotations.spec.ts -describe.skip('Annotations filtering', () => { +describe('Annotations filtering', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -10,9 +9,7 @@ describe.skip('Annotations filtering', () => { it('Tests switching filter type updates the UI accordingly', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID }); - e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); - + e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Annotations').click(); cy.contains('New query').click(); e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type('Red - Panel two'); @@ -38,30 +35,20 @@ describe.skip('Annotations filtering', () => { .type('Panel two{enter}', { force: true }); }); - cy.get('body').click(); + e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click({ force: true }); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); - - e2e.pages.Dashboard.Controls() + e2e.pages.Dashboard.SubMenu.Annotations.annotationsWrapper() .should('be.visible') .within(() => { - e2e.pages.Dashboard.SubMenu.submenuItemLabels('Red - Panel two') - .should('be.visible') - .parent() - .within((el) => { - cy.get('input') - .should('be.checked') - .uncheck({ force: true }) - .should('not.be.checked') - .check({ force: true }); - }); + e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red - Panel two').should('be.visible'); + e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red - Panel two') + .should('be.checked') + .uncheck({ force: true }) + .should('not.be.checked') + .check({ force: true }); - e2e.pages.Dashboard.SubMenu.submenuItemLabels('Red, only panel 1') - .should('be.visible') - .parent() - .within((el) => { - cy.get('input').should('be.checked'); - }); + e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red, only panel 1').should('be.visible'); + e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red, only panel 1').should('be.checked'); }); e2e.components.Panels.Panel.title('Panel one') diff --git a/e2e/scenes/various-suite/frontend-sandbox-app.spec.ts b/e2e/old-arch/various-suite/frontend-sandbox-app.spec.ts similarity index 100% rename from e2e/scenes/various-suite/frontend-sandbox-app.spec.ts rename to e2e/old-arch/various-suite/frontend-sandbox-app.spec.ts diff --git a/e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts b/e2e/old-arch/various-suite/frontend-sandbox-datasource.spec.ts similarity index 97% rename from e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts rename to e2e/old-arch/various-suite/frontend-sandbox-datasource.spec.ts index 651ba170db9..bfc3fd3fdc3 100644 --- a/e2e/scenes/various-suite/frontend-sandbox-datasource.spec.ts +++ b/e2e/old-arch/various-suite/frontend-sandbox-datasource.spec.ts @@ -6,8 +6,7 @@ const DATASOURCE_ID = 'sandbox-test-datasource'; let DATASOURCE_CONNECTION_ID = ''; const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance'; -// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts -describe.skip('Datasource sandbox', () => { +describe('Datasource sandbox', () => { before(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); @@ -86,7 +85,6 @@ describe.skip('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); - // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); @@ -98,6 +96,7 @@ describe.skip('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); + // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); @@ -130,6 +129,7 @@ describe.skip('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); + // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); diff --git a/e2e/scenes/various-suite/gauge.spec.ts b/e2e/old-arch/various-suite/gauge.spec.ts similarity index 100% rename from e2e/scenes/various-suite/gauge.spec.ts rename to e2e/old-arch/various-suite/gauge.spec.ts diff --git a/e2e/scenes/various-suite/graph-auto-migrate.spec.ts b/e2e/old-arch/various-suite/graph-auto-migrate.spec.ts similarity index 100% rename from e2e/scenes/various-suite/graph-auto-migrate.spec.ts rename to e2e/old-arch/various-suite/graph-auto-migrate.spec.ts diff --git a/e2e/scenes/various-suite/helpers/prometheus-helpers.ts b/e2e/old-arch/various-suite/helpers/prometheus-helpers.ts similarity index 100% rename from e2e/scenes/various-suite/helpers/prometheus-helpers.ts rename to e2e/old-arch/various-suite/helpers/prometheus-helpers.ts diff --git a/e2e/scenes/various-suite/inspect-drawer.spec.ts b/e2e/old-arch/various-suite/inspect-drawer.spec.ts similarity index 100% rename from e2e/scenes/various-suite/inspect-drawer.spec.ts rename to e2e/old-arch/various-suite/inspect-drawer.spec.ts diff --git a/e2e/scenes/various-suite/keybinds.spec.ts b/e2e/old-arch/various-suite/keybinds.spec.ts similarity index 88% rename from e2e/scenes/various-suite/keybinds.spec.ts rename to e2e/old-arch/various-suite/keybinds.spec.ts index c3a8b71359d..2f12aee7586 100644 --- a/e2e/scenes/various-suite/keybinds.spec.ts +++ b/e2e/old-arch/various-suite/keybinds.spec.ts @@ -1,8 +1,7 @@ import { e2e } from '../utils'; import { fromBaseUrl } from '../utils/support/url'; -// Skipping due to race conditions with same old arch test e2e/various-suite/keybinds.spec.ts -describe.skip('Keyboard shortcuts', () => { +describe('Keyboard shortcuts', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); @@ -34,10 +33,12 @@ describe.skip('Keyboard shortcuts', () => { zone: 'Browser', }); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); + let expectedRange = `Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00`; + e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); cy.get('body').type('{ctrl}z'); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - let expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`; + expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`; e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); }); diff --git a/e2e/scenes/various-suite/loki-editor.spec.ts b/e2e/old-arch/various-suite/loki-editor.spec.ts similarity index 100% rename from e2e/scenes/various-suite/loki-editor.spec.ts rename to e2e/old-arch/various-suite/loki-editor.spec.ts diff --git a/e2e/scenes/various-suite/loki-query-builder.spec.ts b/e2e/old-arch/various-suite/loki-query-builder.spec.ts similarity index 100% rename from e2e/scenes/various-suite/loki-query-builder.spec.ts rename to e2e/old-arch/various-suite/loki-query-builder.spec.ts diff --git a/e2e/scenes/various-suite/loki-table-explore-to-dash.spec.ts b/e2e/old-arch/various-suite/loki-table-explore-to-dash.spec.ts similarity index 96% rename from e2e/scenes/various-suite/loki-table-explore-to-dash.spec.ts rename to e2e/old-arch/various-suite/loki-table-explore-to-dash.spec.ts index 088745c7fca..c3834e7e7b9 100644 --- a/e2e/scenes/various-suite/loki-table-explore-to-dash.spec.ts +++ b/e2e/old-arch/various-suite/loki-table-explore-to-dash.spec.ts @@ -1,6 +1,6 @@ import { e2e } from '../utils'; -const dataSourceName = 'LokiEditor'; +const dataSourceName = 'LokiEditor' + Date.now(); const addDataSource = () => { e2e.flows.addDataSource({ type: 'Loki', @@ -110,8 +110,7 @@ const lokiQueryResult = { }, }; -// Skipping due to race conditions with same old arch test e2e/various-suite/loki-table-explore-to-dash.spec.ts -describe.skip('Loki Query Editor', () => { +describe('Loki Query Editor', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -122,7 +121,8 @@ describe.skip('Loki Query Editor', () => { beforeEach(() => { cy.window().then((win) => { - win.localStorage.setItem('grafana.featureToggles', 'logsExploreTableVisualisation=1'); + cy.setLocalStorage('grafana.featureToggles', 'logsExploreTableVisualisation=1'); + cy.setLocalStorage('grafana.featureToggles', 'dashboardScene=false'); }); }); it('Should be able to add explore table to dashboard', () => { diff --git a/e2e/scenes/various-suite/navigation.spec.ts b/e2e/old-arch/various-suite/navigation.spec.ts similarity index 100% rename from e2e/scenes/various-suite/navigation.spec.ts rename to e2e/old-arch/various-suite/navigation.spec.ts diff --git a/e2e/scenes/various-suite/pie-chart.spec.ts b/e2e/old-arch/various-suite/pie-chart.spec.ts similarity index 68% rename from e2e/scenes/various-suite/pie-chart.spec.ts rename to e2e/old-arch/various-suite/pie-chart.spec.ts index c6d537a40a2..2030aed53fe 100644 --- a/e2e/scenes/various-suite/pie-chart.spec.ts +++ b/e2e/old-arch/various-suite/pie-chart.spec.ts @@ -11,8 +11,9 @@ describe('Pie Chart Panel', () => { // open Panel Tests - Pie Chart e2e.flows.openDashboard({ uid: 'lVE-2YFMz' }); - cy.get( - `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` - ).should('have.length', 5); + cy.get(`[data-panelid=11] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]`).should( + 'have.length', + 5 + ); }); }); diff --git a/e2e/scenes/various-suite/prometheus-annotations.spec.ts b/e2e/old-arch/various-suite/prometheus-annotations.spec.ts similarity index 90% rename from e2e/scenes/various-suite/prometheus-annotations.spec.ts rename to e2e/old-arch/various-suite/prometheus-annotations.spec.ts index 91392a145a1..b32620dda70 100644 --- a/e2e/scenes/various-suite/prometheus-annotations.spec.ts +++ b/e2e/old-arch/various-suite/prometheus-annotations.spec.ts @@ -14,8 +14,7 @@ const DATASOURCE_NAME = 'aprometheusAnnotationDS'; * */ function navigateToAnnotations() { - e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); + e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Annotations').click(); } @@ -68,7 +67,7 @@ describe('Prometheus annotations', () => { // series value as timestamp e2e.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp().scrollIntoView().should('exist'); - e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); + e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click(); // check that annotation exists cy.get('body').contains(annotationName); diff --git a/e2e/scenes/various-suite/prometheus-config.spec.ts b/e2e/old-arch/various-suite/prometheus-config.spec.ts similarity index 100% rename from e2e/scenes/various-suite/prometheus-config.spec.ts rename to e2e/old-arch/various-suite/prometheus-config.spec.ts diff --git a/e2e/scenes/various-suite/prometheus-editor.spec.ts b/e2e/old-arch/various-suite/prometheus-editor.spec.ts similarity index 93% rename from e2e/scenes/various-suite/prometheus-editor.spec.ts rename to e2e/old-arch/various-suite/prometheus-editor.spec.ts index ffe8eee9894..d04d502a706 100644 --- a/e2e/scenes/various-suite/prometheus-editor.spec.ts +++ b/e2e/old-arch/various-suite/prometheus-editor.spec.ts @@ -1,5 +1,3 @@ -import { selectors } from '@grafana/e2e-selectors'; - import { e2e } from '../utils'; import { getResources } from './helpers/prometheus-helpers'; @@ -43,8 +41,8 @@ function navigateToEditor(editorType: editorType, name: string): void { e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(name).scrollIntoView().should('be.visible').click(); } -// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/prometheus-editor.spec.ts -describe.skip('Prometheus query editor', () => { + +describe('Prometheus query editor', () => { it('should have a kickstart component', () => { navigateToEditor('Code', 'prometheus'); e2e.components.QueryBuilder.queryPatterns().scrollIntoView().should('exist'); @@ -67,9 +65,9 @@ describe.skip('Prometheus query editor', () => { // check options e2e.components.DataSource.Prometheus.queryEditor.legend().scrollIntoView().should('exist'); e2e.components.DataSource.Prometheus.queryEditor.format().scrollIntoView().should('exist'); - cy.get(`#${selectors.components.DataSource.Prometheus.queryEditor.step}`).scrollIntoView().should('exist'); + cy.get(`[data-test-id="prometheus-step"]`).scrollIntoView().should('exist'); e2e.components.DataSource.Prometheus.queryEditor.type().scrollIntoView().should('exist'); - cy.get(`#${selectors.components.DataSource.Prometheus.queryEditor.exemplars}`).scrollIntoView().should('exist'); + cy.get(`[data-test-id="prometheus-exemplars"]`).scrollIntoView().should('exist'); }); describe('Code editor', () => { diff --git a/e2e/scenes/various-suite/prometheus-variable-editor.spec.ts b/e2e/old-arch/various-suite/prometheus-variable-editor.spec.ts similarity index 90% rename from e2e/scenes/various-suite/prometheus-variable-editor.spec.ts rename to e2e/old-arch/various-suite/prometheus-variable-editor.spec.ts index 4b6c62427aa..8e91ce8c283 100644 --- a/e2e/scenes/various-suite/prometheus-variable-editor.spec.ts +++ b/e2e/old-arch/various-suite/prometheus-variable-editor.spec.ts @@ -11,8 +11,7 @@ const DATASOURCE_NAME = 'prometheusVariableDS'; * Click dashboard settings and then the variables tab */ function navigateToVariables() { - e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); - e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); + e2e.components.PageToolbar.item('Dashboard settings').click(); e2e.components.Tab.title('Variables').click(); } @@ -50,7 +49,7 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) { e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); // close to return to dashboard - e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); + e2e.pages.Dashboard.Settings.Actions.close().click(); // add visualization e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible').click(); @@ -77,8 +76,8 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) { // do nothing } } -// Skipping due to race conditions with same old arch test e2e/various-suite/prometheus-variable-editor.spec.ts -describe.skip('Prometheus variable query editor', () => { + +describe('Prometheus variable query editor', () => { beforeEach(() => { createPromDS(DATASOURCE_ID, DATASOURCE_NAME); }); diff --git a/e2e/scenes/various-suite/query-editor.spec.ts b/e2e/old-arch/various-suite/query-editor.spec.ts similarity index 100% rename from e2e/scenes/various-suite/query-editor.spec.ts rename to e2e/old-arch/various-suite/query-editor.spec.ts diff --git a/e2e/scenes/various-suite/return-to-previous.spec.ts b/e2e/old-arch/various-suite/return-to-previous.spec.ts similarity index 100% rename from e2e/scenes/various-suite/return-to-previous.spec.ts rename to e2e/old-arch/various-suite/return-to-previous.spec.ts diff --git a/e2e/scenes/various-suite/select-focus.spec.ts b/e2e/old-arch/various-suite/select-focus.spec.ts similarity index 100% rename from e2e/scenes/various-suite/select-focus.spec.ts rename to e2e/old-arch/various-suite/select-focus.spec.ts diff --git a/e2e/scenes/various-suite/solo-route.spec.ts b/e2e/old-arch/various-suite/solo-route.spec.ts similarity index 100% rename from e2e/scenes/various-suite/solo-route.spec.ts rename to e2e/old-arch/various-suite/solo-route.spec.ts diff --git a/e2e/scenes/various-suite/trace-view-scrolling.spec.ts b/e2e/old-arch/various-suite/trace-view-scrolling.spec.ts similarity index 100% rename from e2e/scenes/various-suite/trace-view-scrolling.spec.ts rename to e2e/old-arch/various-suite/trace-view-scrolling.spec.ts diff --git a/e2e/scenes/various-suite/visualization-suggestions.spec.ts b/e2e/old-arch/various-suite/visualization-suggestions.spec.ts similarity index 100% rename from e2e/scenes/various-suite/visualization-suggestions.spec.ts rename to e2e/old-arch/various-suite/visualization-suggestions.spec.ts diff --git a/e2e/panels-suite/dashlist.spec.ts b/e2e/panels-suite/dashlist.spec.ts index ec053d3e5e4..002cf235e82 100644 --- a/e2e/panels-suite/dashlist.spec.ts +++ b/e2e/panels-suite/dashlist.spec.ts @@ -20,7 +20,11 @@ describe('DashList panel', () => { }); // update variable to b - e2e.pages.Dashboard.SubMenu.submenuItemLabels('server').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('server') + .parent() + .within(() => { + cy.get('input').click(); + }); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click(); // blur the dropdown cy.get('body').click(); diff --git a/e2e/panels-suite/frontend-sandbox-panel.spec.ts b/e2e/panels-suite/frontend-sandbox-panel.spec.ts index bab59b99866..b551617b705 100644 --- a/e2e/panels-suite/frontend-sandbox-panel.spec.ts +++ b/e2e/panels-suite/frontend-sandbox-panel.spec.ts @@ -2,8 +2,8 @@ import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json'; import { e2e } from '../utils'; const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950'; - -describe('Panel sandbox', () => { +// Skipping due to race conditions with same old arch test e2e/panels-suite/frontend-sandbox-panel.spec.ts +describe.skip('Panel sandbox', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true); diff --git a/e2e/panels-suite/geomap-layer-types.spec.ts b/e2e/panels-suite/geomap-layer-types.spec.ts index 17779dde1c9..70af145c784 100644 --- a/e2e/panels-suite/geomap-layer-types.spec.ts +++ b/e2e/panels-suite/geomap-layer-types.spec.ts @@ -21,41 +21,41 @@ describe('Geomap layer types', () => { e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Heatmap{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('heatmap'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // GeoJSON e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('GeoJSON{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('geojson'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // Open Street Map e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Open Street Map{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('osm-standard'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist'); e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('not.exist'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // CARTO basemap e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('CARTO basemap{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('carto'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // ArcGIS MapServer e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('ArcGIS MapServer{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('esri-xyz'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); // XYZ Tile layer e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('XYZ Tile layer{enter}'); cy.get('[data-testid="layer-drag-drop-list"]').contains('xyz'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution').should('be.visible'); - e2e.components.PanelEditor.General.content().should('be.visible'); + // e2e.components.PanelEditor.General.content().should('be.visible'); }); it.skip('Tests changing the layer type (alpha)', () => { diff --git a/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts b/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts index 52ca7ed0fac..bce8268b36a 100644 --- a/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts +++ b/e2e/panels-suite/geomap-spatial-operations-transform.spec.ts @@ -9,7 +9,7 @@ describe('Geomap spatial operations', () => { it('Tests location auto option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -27,7 +27,7 @@ describe('Geomap spatial operations', () => { it('Tests location coords option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -51,7 +51,7 @@ describe('Geomap spatial operations', () => { it('Tests geoshash field column appears in table view', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); @@ -74,7 +74,7 @@ describe('Geomap spatial operations', () => { it('Tests location lookup option', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click(); diff --git a/e2e/panels-suite/panelEdit_base.spec.ts b/e2e/panels-suite/panelEdit_base.spec.ts index 744c41ed8ce..97eebe35777 100644 --- a/e2e/panels-suite/panelEdit_base.spec.ts +++ b/e2e/panels-suite/panelEdit_base.spec.ts @@ -1,3 +1,5 @@ +import { selectors } from '@grafana/e2e-selectors'; + import { e2e } from '../utils'; const PANEL_UNDER_TEST = 'Lines 500 data points'; @@ -16,62 +18,53 @@ describe('Panel edit tests', () => { e2e.flows.openPanelMenuItem(e2e.flows.PanelMenuItems.Edit, PANEL_UNDER_TEST); - // New panel editor opens when navigating from Panel menu + // // New panel editor opens when navigating from Panel menu e2e.components.PanelEditor.General.content().should('be.visible'); // Queries tab is rendered and open by default e2e.components.PanelEditor.DataPane.content() + .scrollIntoView() .should('be.visible') .within(() => { - e2e.components.Tab.title('Query').should('be.visible'); + e2e.components.Tab.title('Queries').should('be.visible'); // data should be the active tab e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Query1'); // there's already a query so therefore Query + 1 + expect(li.text()).equals('Queries1'); // there's already a query so therefore Query + 1 }); - e2e.components.QueryTab.content().should('be.visible'); + // cy.get('[data-testid]="query-editor-rows"').should('be.visible'); + cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('be.visible'); e2e.components.TransformTab.content().should('not.exist'); e2e.components.AlertTab.content().should('not.exist'); e2e.components.PanelAlertTabContent.content().should('not.exist'); // Bottom pane tabs // Can change to Transform tab - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Tab.active().within((li: JQuery) => { - expect(li.text()).equals('Transform data0'); // there's no transform so therefore Transform + 0 + expect(li.text()).equals('Transformations0'); // there's no transform so therefore Transform + 0 }); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible'); - e2e.components.QueryTab.content().should('not.exist'); + cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); e2e.components.AlertTab.content().should('not.exist'); e2e.components.PanelAlertTabContent.content().should('not.exist'); // Can change to Alerts tab (graph panel is the default vis so the alerts tab should be rendered) - e2e.components.Tab.title('Alert').should('be.visible').click(); + e2e.components.Tab.title('Alert').scrollIntoView().should('be.visible').click(); e2e.components.Tab.active().should('have.text', 'Alert0'); // there's no alert so therefore Alert + 0 // Needs to be disabled until Grafana EE turns unified alerting on by default // e2e.components.AlertTab.content().should('not.exist'); - e2e.components.QueryTab.content().should('not.exist'); + cy.get(`[data-testid="${selectors.components.QueryTab.content}"]`).should('not.exist'); e2e.components.TransformTab.content().should('not.exist'); // Needs to be disabled until Grafana EE turns unified alerting on by default // e2e.components.PanelAlertTabContent.content().should('exist'); // e2e.components.PanelAlertTabContent.content().should('be.visible'); - e2e.components.Tab.title('Query').should('be.visible').click(); + e2e.components.Tab.title('Queries').should('be.visible').click(); }); - // Panel sidebar is rendered open by default - e2e.components.PanelEditor.OptionsPane.content().should('be.visible'); - - // close options pane - e2e.components.PanelEditor.toggleVizOptions().click(); - e2e.components.PanelEditor.OptionsPane.content().should('not.exist'); - - // open options pane - e2e.components.PanelEditor.toggleVizOptions().should('be.visible').click(); - e2e.components.PanelEditor.OptionsPane.content().should('be.visible'); - // Check that Time series is chosen e2e.components.PanelEditor.toggleVizPicker().click(); e2e.components.PluginVisualization.item('Time series').should('be.visible'); @@ -102,6 +95,8 @@ describe('Panel edit tests', () => { e2e.components.PanelEditor.DataPane.content().should('be.visible'); // Field & Overrides tabs (need to switch to React based vis, i.e. Table) + e2e.components.PanelEditor.toggleTableView().click({ force: true }).click({ force: true }); + e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible'); }); diff --git a/e2e/panels-suite/panelEdit_transforms.spec.ts b/e2e/panels-suite/panelEdit_transforms.spec.ts index eb3029b8d98..963991a22e7 100644 --- a/e2e/panels-suite/panelEdit_transforms.spec.ts +++ b/e2e/panels-suite/panelEdit_transforms.spec.ts @@ -8,7 +8,7 @@ describe('Panel edit tests - transformations', () => { it('Tests transformations editor', () => { e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); e2e.components.Transforms.Reduce.calculationsLabel().scrollIntoView().should('be.visible'); @@ -18,7 +18,7 @@ describe('Panel edit tests - transformations', () => { it('Tests case where transformations can be disabled and not clear out panel data', () => { e2e.flows.openDashboard({ uid: 'TkZXxlNG3', queryParams: { editPanel: 47 } }); - e2e.components.Tab.title('Transform data').should('be.visible').click(); + e2e.components.Tab.title('Transformations').should('be.visible').click(); e2e.components.Transforms.addTransformationButton().scrollIntoView().should('be.visible').click(); e2e.components.TransformTab.newTransform('Reduce').scrollIntoView().should('be.visible').click(); e2e.components.Transforms.disableTransformationButton().should('be.visible').click(); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.ts new file mode 100644 index 00000000000..b37ab2722bf --- /dev/null +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/dashboard.ts @@ -0,0 +1,15 @@ +import { expect, test } from '@grafana/plugin-e2e'; + +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; + +test('add panel in already existing dashboard', async ({ gotoDashboardPage, page }) => { + const dashboardPage = await gotoDashboardPage(REACT_TABLE_DASHBOARD); + await dashboardPage.addPanel(); + await expect(page.url()).toContain('editPanel'); +}); + +test('add panel in new dashboard', async ({ dashboardPage, page }) => { + const panelEditPage = await dashboardPage.addPanel(); + await expect(panelEditPage.panel.locator).toBeVisible(); + await expect(page.url()).toContain('editPanel'); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts index 13c74023043..b700c247006 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -1,4 +1,4 @@ -import { DashboardPage, expect, test } from '@grafana/plugin-e2e'; +import { expect, test } from '@grafana/plugin-e2e'; import { formatExpectError } from '../errors'; import { successfulDataQuery } from '../mocks/queries'; @@ -8,6 +8,7 @@ const PANEL_TITLE = 'Table panel E2E test'; const TABLE_VIZ_NAME = 'Table'; const STANDARD_OTIONS_CATEGORY = 'Standard options'; const DISPLAY_NAME_LABEL = 'Display name'; +const REACT_TABLE_DASHBOARD = { uid: 'U_bZIMRMk' }; test.describe('query editor query data', () => { test('query data response should be OK when query is valid', async ({ panelEditPage }) => { @@ -84,3 +85,9 @@ test.describe('edit panel plugin settings', () => { ).toBeVisible(); }); }); + +test('backToDashboard method should navigate to dashboard page', async ({ gotoPanelEditPage, page }) => { + const panelEditPage = await gotoPanelEditPage({ dashboard: REACT_TABLE_DASHBOARD, id: '4' }); + await panelEditPage.backToDashboard(); + await expect(page.url()).not.toContain('editPanel'); +}); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts index 75e7b943536..b93d9a8e5dd 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-admin-user/variableEditPage.spec.ts @@ -5,6 +5,7 @@ import { prometheusLabels } from '../mocks/resources'; test('variable query with mocked response', async ({ variableEditPage, page }) => { variableEditPage.mockResourceResponse('api/v1/labels?*', prometheusLabels); + variableEditPage.mockResourceResponse('suggestions*', prometheusLabels); await variableEditPage.datasource.set('gdev-prometheus'); await variableEditPage.getByGrafanaSelector('Query type').fill('Label names'); await page.keyboard.press('Tab'); diff --git a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts index 6346151f2cd..7c7711fa625 100644 --- a/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts +++ b/e2e/plugin-e2e/plugin-e2e-api-tests/as-viewer-user/permissions.spec.ts @@ -2,9 +2,10 @@ import { expect, test } from '@grafana/plugin-e2e'; test('should redirect to start page when permissions to navigate to page is missing', async ({ page }) => { await page.goto('/'); - const homePageTitle = await page.title(); + const homePageURL = new URL(page.url()); await page.goto('/datasources', { waitUntil: 'networkidle' }); - expect(await page.title()).toEqual(homePageTitle); + const redirectedPageURL = new URL(page.url()); + expect(homePageURL.pathname).toEqual(redirectedPageURL.pathname); }); test('current user should have viewer role', async ({ page, request }) => { diff --git a/e2e/run-suite b/e2e/run-suite index 38770c2becb..11569b43982 100755 --- a/e2e/run-suite +++ b/e2e/run-suite @@ -27,7 +27,7 @@ declare -A env=( testFilesForSingleSuite="*.spec.ts" rootForEnterpriseSuite="./e2e/extensions-suite" -rootForScenesSuite="./e2e/scenes" +rootForOldArch="./e2e/old-arch" declare -A cypressConfig=( [screenshotsFolder]=./e2e/"${args[0]}"/screenshots @@ -88,9 +88,9 @@ case "$1" in ;; "") ;; - "scenes") - env[SCENES]=true - cypressConfig[specPattern]=$rootForScenesSuite/*/$testFilesForSingleSuite + "old-arch") + env[DISABLE_SCENES]=true + cypressConfig[specPattern]=$rootForOldArch/*/$testFilesForSingleSuite cypressConfig[video]=false case "$2" in "debug") @@ -106,10 +106,10 @@ case "$1" in ;; esac ;; - "scenes/"*) + "old-arch/"*) cypressConfig[specPattern]=./e2e/"${args[0]}"/$testFilesForSingleSuite cypressConfig[video]=${args[1]} - env[SCENES]=true + env[DISABLE_SCENES]=true ;; "enterprise-smtp") env[SMTP_PLUGIN_ENABLED]=true diff --git a/e2e/shared/smokeTestScenario.ts b/e2e/shared/smokeTestScenario.ts index 4faee58a237..e45870283ec 100644 --- a/e2e/shared/smokeTestScenario.ts +++ b/e2e/shared/smokeTestScenario.ts @@ -6,6 +6,11 @@ export const smokeTestScenario = () => e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), false); e2e.flows.addDataSource(); e2e.flows.addDashboard(); + e2e.flows.addPanel({ + dataSourceName: 'gdev-testdata', + visitDashboardAtStart: false, + timeout: 10000, + }); }); after(() => { @@ -13,11 +18,6 @@ export const smokeTestScenario = () => }); it('Login scenario, create test data source, dashboard, panel, and export scenario', () => { - // wait for time to be set to account for any layout shift - cy.contains('2020-01-01 00:00:00 to 2020-01-01 06:00:00').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add button').click(); - e2e.components.PageToolbar.itemButton('Add new visualization menu item').click(); - e2e.components.DataSource.TestData.QueryTab.scenarioSelectContainer() .should('be.visible') .within(() => { @@ -29,8 +29,7 @@ export const smokeTestScenario = () => // Make sure the graph renders via checking legend e2e.components.VizLegend.seriesName('A-series').should('be.visible'); - // Expand options section - e2e.components.PanelEditor.applyButton(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().click(); // Make sure panel is & visualization is added to dashboard e2e.components.VizLegend.seriesName('A-series').should('be.visible'); diff --git a/e2e/smoke-tests-suite/panels_smokescreen.spec.ts b/e2e/smoke-tests-suite/panels_smokescreen.spec.ts index 5ba784d72b2..ca63dadba19 100644 --- a/e2e/smoke-tests-suite/panels_smokescreen.spec.ts +++ b/e2e/smoke-tests-suite/panels_smokescreen.spec.ts @@ -14,17 +14,11 @@ describe('Panels smokescreen', () => { it('Tests each panel type in the panel edit view to ensure no crash', () => { e2e.flows.addDashboard(); - // TODO: Try and use e2e.flows.addPanel() instead of block below - try { - e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add button').click(); - } catch (e) { - // Depending on the screen size, the "Add panel" button might be hidden - e2e.components.PageToolbar.item('Show more items').click(); - e2e.components.PageToolbar.item('Add button').last().click(); - } - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); + e2e.flows.addPanel({ + dataSourceName: 'gdev-testdata', + timeout: 10000, + visitDashboardAtStart: false, + }); cy.window().then((win: Cypress.AUTWindow & { grafanaBootData: GrafanaBootConfig['bootData'] }) => { // Loop through every panel type and ensure no crash diff --git a/e2e/test-plugins/grafana-extensionstest-app/package.json b/e2e/test-plugins/grafana-extensionstest-app/package.json index 0da20ae736b..f7577e54dd5 100644 --- a/e2e/test-plugins/grafana-extensionstest-app/package.json +++ b/e2e/test-plugins/grafana-extensionstest-app/package.json @@ -23,7 +23,7 @@ "glob": "10.4.1", "ts-node": "10.9.2", "typescript": "5.5.4", - "webpack": "5.91.0", + "webpack": "5.95.0", "webpack-merge": "5.10.0" }, "engines": { diff --git a/e2e/utils/flows/addDashboard.ts b/e2e/utils/flows/addDashboard.ts index cc8e7548032..77dd07e9c9b 100644 --- a/e2e/utils/flows/addDashboard.ts +++ b/e2e/utils/flows/addDashboard.ts @@ -139,9 +139,9 @@ export const addDashboard = (config?: Partial) => { setDashboardTimeRange(timeRange); - e2e.components.PageToolbar.item('Save dashboard').click(); - e2e.pages.SaveDashboardAsModal.newName().clear().type(title, { force: true }); - e2e.pages.SaveDashboardAsModal.save().click(); + e2e.components.NavToolbar.editDashboard.saveButton().click(); + e2e.components.Drawer.DashboardSaveDrawer.saveAsTitleInput().clear().type(title, { force: true }); + e2e.components.Drawer.DashboardSaveDrawer.saveButton().click(); e2e.flows.assertSuccessNotification(); e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible'); diff --git a/e2e/utils/flows/configurePanel.ts b/e2e/utils/flows/configurePanel.ts index 4aa6a6cc5ab..7a881a47c17 100644 --- a/e2e/utils/flows/configurePanel.ts +++ b/e2e/utils/flows/configurePanel.ts @@ -85,15 +85,17 @@ export const configurePanel = (config: PartialAddPanelConfig | PartialEditPanelC e2e.components.Panels.Panel.headerItems('Edit').click(); } else { try { - e2e.components.PageToolbar.itemButton('Add button').should('be.visible'); - e2e.components.PageToolbar.itemButton('Add button').click(); + //Enter edit mode + e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); + e2e.components.PageToolbar.itemButton('Add button').should('be.visible').click(); + e2e.components.NavToolbar.editDashboard.addVisualizationButton().should('be.visible').click(); } catch (e) { // Depending on the screen size, the "Add" button might be hidden e2e.components.PageToolbar.item('Show more items').click(); e2e.components.PageToolbar.item('Add button').last().click(); } - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); - e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); + // e2e.pages.AddDashboard.itemButton('Add new visualization menu item').should('be.visible'); + // e2e.pages.AddDashboard.itemButton('Add new visualization menu item').click(); } if (timeRange) { diff --git a/e2e/utils/flows/importDashboard.ts b/e2e/utils/flows/importDashboard.ts index 4e7b8ade4ad..d3c6b7e23b5 100644 --- a/e2e/utils/flows/importDashboard.ts +++ b/e2e/utils/flows/importDashboard.ts @@ -51,7 +51,9 @@ export const importDashboard = (dashboardToImport: Dashboard, queryTimeout?: num e2e.components.Panels.Panel.menu(panel.title).click({ force: true }); // force click because menu is hidden and show on hover e2e.components.Panels.Panel.menuItems('Inspect').should('be.visible').click(); e2e.components.Tab.title('JSON').should('be.visible').click(); - e2e.components.PanelInspector.Json.content().should('be.visible').contains('Panel JSON').click({ force: true }); + e2e.components.PanelInspector.Json.content().should('be.visible'); + e2e.components.ReactMonacoEditor.editorLazy().should('be.visible'); + cy.contains('Panel JSON').click({ force: true }); e2e.components.Select.option().should('be.visible').contains('Panel data').click(); // ensures that panel has loaded without knowingly hitting an error diff --git a/e2e/utils/support/url.ts b/e2e/utils/support/url.ts index 94e42827940..34620974b06 100644 --- a/e2e/utils/support/url.ts +++ b/e2e/utils/support/url.ts @@ -1,3 +1,5 @@ +import { e2e } from '../index'; + const getBaseUrl = () => Cypress.env('BASE_URL') || Cypress.config().baseUrl || 'http://localhost:3000'; export const fromBaseUrl = (url = '') => new URL(url, getBaseUrl()).href; diff --git a/e2e/various-suite/bar-gauge.spec.ts b/e2e/various-suite/bar-gauge.spec.ts index 5f8506b8050..7c28c48a146 100644 --- a/e2e/various-suite/bar-gauge.spec.ts +++ b/e2e/various-suite/bar-gauge.spec.ts @@ -11,7 +11,9 @@ describe('Bar Gauge Panel', () => { // open Panel Tests - Bar Gauge e2e.flows.openDashboard({ uid: 'O6f11TZWk' }); - cy.get(`[data-panelid=6] [data-testid^="${selectors.components.Panels.Visualization.BarGauge.valueV2}"]`) + cy.get( + `[data-viz-panel-key="panel-6"] [data-testid^="${selectors.components.Panels.Visualization.BarGauge.valueV2}"]` + ) .should('have.css', 'color', 'rgb(242, 73, 92)') .contains('100'); }); diff --git a/e2e/various-suite/exemplars.spec.ts b/e2e/various-suite/exemplars.spec.ts index b3c422c1294..675b83039ba 100644 --- a/e2e/various-suite/exemplars.spec.ts +++ b/e2e/various-suite/exemplars.spec.ts @@ -17,8 +17,8 @@ const addDataSource = () => { }, }); }; - -describe('Exemplars', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/exemplars.spec.ts +describe.skip('Exemplars', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); @@ -69,7 +69,7 @@ describe('Exemplars', () => { cy.get(`[data-testid="time-series-zoom-to-data"]`).click(); - e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove'); + e2e.components.DataSource.Prometheus.exemplarMarker().first().trigger('mousemove', { force: true }); cy.contains('Query with gdev-tempo').click(); e2e.components.TraceViewer.spanBar().should('have.length', 11); }); diff --git a/e2e/various-suite/filter-annotations.spec.ts b/e2e/various-suite/filter-annotations.spec.ts index 26cf83f00e2..35319ee1beb 100644 --- a/e2e/various-suite/filter-annotations.spec.ts +++ b/e2e/various-suite/filter-annotations.spec.ts @@ -1,7 +1,8 @@ import { e2e } from '../utils'; const DASHBOARD_ID = 'ed155665'; -describe('Annotations filtering', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/filter-annotations.spec.ts +describe.skip('Annotations filtering', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); @@ -9,7 +10,9 @@ describe('Annotations filtering', () => { it('Tests switching filter type updates the UI accordingly', () => { e2e.flows.openDashboard({ uid: DASHBOARD_ID }); - e2e.components.PageToolbar.item('Dashboard settings').click(); + e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); + e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); + e2e.components.Tab.title('Annotations').click(); cy.contains('New query').click(); e2e.pages.Dashboard.Settings.Annotations.Settings.name().clear().type('Red - Panel two'); @@ -35,20 +38,30 @@ describe('Annotations filtering', () => { .type('Panel two{enter}', { force: true }); }); - e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click({ force: true }); + cy.get('body').click(); - e2e.pages.Dashboard.SubMenu.Annotations.annotationsWrapper() + e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); + + e2e.pages.Dashboard.Controls() .should('be.visible') .within(() => { - e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red - Panel two').should('be.visible'); - e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red - Panel two') - .should('be.checked') - .uncheck({ force: true }) - .should('not.be.checked') - .check({ force: true }); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('Red - Panel two') + .should('be.visible') + .parent() + .within((el) => { + cy.get('input') + .should('be.checked') + .uncheck({ force: true }) + .should('not.be.checked') + .check({ force: true }); + }); - e2e.pages.Dashboard.SubMenu.Annotations.annotationLabel('Red, only panel 1').should('be.visible'); - e2e.pages.Dashboard.SubMenu.Annotations.annotationToggle('Red, only panel 1').should('be.checked'); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('Red, only panel 1') + .should('be.visible') + .parent() + .within((el) => { + cy.get('input').should('be.checked'); + }); }); e2e.components.Panels.Panel.title('Panel one') diff --git a/e2e/various-suite/frontend-sandbox-datasource.spec.ts b/e2e/various-suite/frontend-sandbox-datasource.spec.ts index bfc3fd3fdc3..651ba170db9 100644 --- a/e2e/various-suite/frontend-sandbox-datasource.spec.ts +++ b/e2e/various-suite/frontend-sandbox-datasource.spec.ts @@ -6,7 +6,8 @@ const DATASOURCE_ID = 'sandbox-test-datasource'; let DATASOURCE_CONNECTION_ID = ''; const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance'; -describe('Datasource sandbox', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/frontend-sandbox-datasource.spec.ts +describe.skip('Datasource sandbox', () => { before(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true); @@ -85,6 +86,7 @@ describe('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); + // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); @@ -96,7 +98,6 @@ describe('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); - // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); @@ -129,7 +130,6 @@ describe('Datasource sandbox', () => { e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click(); - // make sure the datasource was correctly selected and rendered e2e.components.Breadcrumbs.breadcrumb(DATASOURCE_TYPED_NAME).should('be.visible'); diff --git a/e2e/various-suite/keybinds.spec.ts b/e2e/various-suite/keybinds.spec.ts index 2f12aee7586..c3a8b71359d 100644 --- a/e2e/various-suite/keybinds.spec.ts +++ b/e2e/various-suite/keybinds.spec.ts @@ -1,7 +1,8 @@ import { e2e } from '../utils'; import { fromBaseUrl } from '../utils/support/url'; -describe('Keyboard shortcuts', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/keybinds.spec.ts +describe.skip('Keyboard shortcuts', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); @@ -33,12 +34,10 @@ describe('Keyboard shortcuts', () => { zone: 'Browser', }); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - let expectedRange = `Time range selected: 2024-06-05 10:05:00 to 2024-06-05 10:06:00`; - e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); cy.get('body').type('{ctrl}z'); e2e.components.RefreshPicker.runButtonV2().should('have.text', 'Run query'); - expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`; + let expectedRange = `Time range selected: 2024-06-05 10:03:30 to 2024-06-05 10:07:30`; e2e.components.TimePicker.openButton().should('have.attr', 'aria-label', expectedRange); }); diff --git a/e2e/various-suite/loki-table-explore-to-dash.spec.ts b/e2e/various-suite/loki-table-explore-to-dash.spec.ts index c56aa16b578..088745c7fca 100644 --- a/e2e/various-suite/loki-table-explore-to-dash.spec.ts +++ b/e2e/various-suite/loki-table-explore-to-dash.spec.ts @@ -110,7 +110,8 @@ const lokiQueryResult = { }, }; -describe('Loki Query Editor', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/loki-table-explore-to-dash.spec.ts +describe.skip('Loki Query Editor', () => { beforeEach(() => { e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); }); diff --git a/e2e/various-suite/pie-chart.spec.ts b/e2e/various-suite/pie-chart.spec.ts index 2030aed53fe..c6d537a40a2 100644 --- a/e2e/various-suite/pie-chart.spec.ts +++ b/e2e/various-suite/pie-chart.spec.ts @@ -11,9 +11,8 @@ describe('Pie Chart Panel', () => { // open Panel Tests - Pie Chart e2e.flows.openDashboard({ uid: 'lVE-2YFMz' }); - cy.get(`[data-panelid=11] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]`).should( - 'have.length', - 5 - ); + cy.get( + `[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` + ).should('have.length', 5); }); }); diff --git a/e2e/various-suite/prometheus-annotations.spec.ts b/e2e/various-suite/prometheus-annotations.spec.ts index b32620dda70..91392a145a1 100644 --- a/e2e/various-suite/prometheus-annotations.spec.ts +++ b/e2e/various-suite/prometheus-annotations.spec.ts @@ -14,7 +14,8 @@ const DATASOURCE_NAME = 'aprometheusAnnotationDS'; * */ function navigateToAnnotations() { - e2e.components.PageToolbar.item('Dashboard settings').click(); + e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); + e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); e2e.components.Tab.title('Annotations').click(); } @@ -67,7 +68,7 @@ describe('Prometheus annotations', () => { // series value as timestamp e2e.components.DataSource.Prometheus.annotations.seriesValueAsTimestamp().scrollIntoView().should('exist'); - e2e.pages.Dashboard.Settings.Annotations.NewAnnotation.previewInDashboard().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); // check that annotation exists cy.get('body').contains(annotationName); diff --git a/e2e/various-suite/prometheus-editor.spec.ts b/e2e/various-suite/prometheus-editor.spec.ts index d04d502a706..09ae51e4f7f 100644 --- a/e2e/various-suite/prometheus-editor.spec.ts +++ b/e2e/various-suite/prometheus-editor.spec.ts @@ -41,8 +41,8 @@ function navigateToEditor(editorType: editorType, name: string): void { e2e.components.DataSourcePicker.container().should('be.visible').click(); cy.contains(name).scrollIntoView().should('be.visible').click(); } - -describe('Prometheus query editor', () => { +// Skipping due to flakiness/race conditions with same old arch test e2e/various-suite/prometheus-editor.spec.ts +describe.skip('Prometheus query editor', () => { it('should have a kickstart component', () => { navigateToEditor('Code', 'prometheus'); e2e.components.QueryBuilder.queryPatterns().scrollIntoView().should('exist'); diff --git a/e2e/various-suite/prometheus-variable-editor.spec.ts b/e2e/various-suite/prometheus-variable-editor.spec.ts index 8e91ce8c283..4b6c62427aa 100644 --- a/e2e/various-suite/prometheus-variable-editor.spec.ts +++ b/e2e/various-suite/prometheus-variable-editor.spec.ts @@ -11,7 +11,8 @@ const DATASOURCE_NAME = 'prometheusVariableDS'; * Click dashboard settings and then the variables tab */ function navigateToVariables() { - e2e.components.PageToolbar.item('Dashboard settings').click(); + e2e.components.NavToolbar.editDashboard.editButton().should('be.visible').click(); + e2e.components.NavToolbar.editDashboard.settingsButton().should('be.visible').click(); e2e.components.Tab.title('Variables').click(); } @@ -49,7 +50,7 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) { e2e.pages.Dashboard.Settings.Variables.Edit.General.applyButton().click(); // close to return to dashboard - e2e.pages.Dashboard.Settings.Actions.close().click(); + e2e.components.NavToolbar.editDashboard.backToDashboardButton().should('be.visible').click(); // add visualization e2e.pages.AddDashboard.itemButton('Create new panel button').should('be.visible').click(); @@ -76,8 +77,8 @@ function variableFlowToQueryEditor(variableName: string, queryType: string) { // do nothing } } - -describe('Prometheus variable query editor', () => { +// Skipping due to race conditions with same old arch test e2e/various-suite/prometheus-variable-editor.spec.ts +describe.skip('Prometheus variable query editor', () => { beforeEach(() => { createPromDS(DATASOURCE_ID, DATASOURCE_NAME); }); diff --git a/go.mod b/go.mod index 3d55ba923c1..2454c952fd3 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/apache/arrow/go/v15 v15.0.2 // @grafana/observability-metrics github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad github.com/aws/aws-sdk-go v1.55.5 // @grafana/aws-datasources - github.com/beevik/etree v1.2.0 // @grafana/grafana-backend-group + github.com/beevik/etree v1.4.1 // @grafana/grafana-backend-group github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-release-guild github.com/blugelabs/bluge v0.1.9 // @grafana/grafana-backend-group @@ -73,7 +73,7 @@ require ( github.com/googleapis/gax-go/v2 v2.13.0 // @grafana/grafana-backend-group github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.0 // @grafana/grafana-app-platform-squad - github.com/grafana/alerting v0.0.0-20240926144415-27f4e81b4b6b // @grafana/alerting-backend + github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e // @grafana/alerting-backend github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 // @grafana/identity-access-team github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd // @grafana/identity-access-team github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad @@ -83,12 +83,12 @@ require ( github.com/grafana/dskit v0.0.0-20240311184239-73feada6c0d7 // @grafana/grafana-backend-group github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e // @grafana/grafana-operator-experience-squad - github.com/grafana/grafana-aws-sdk v0.31.2 // @grafana/aws-datasources + github.com/grafana/grafana-aws-sdk v0.31.3 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group - github.com/grafana/grafana-plugin-sdk-go v0.250.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.251.0 // @grafana/plugins-platform-backend github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20240821155123-6891eb1d35da // @grafana/grafana-app-platform-squad @@ -159,15 +159,15 @@ require ( github.com/yudai/gojsondiff v1.0.0 // @grafana/grafana-backend-group go.opentelemetry.io/collector/pdata v1.6.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // @grafana/plugins-platform-backend - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 // @grafana/grafana-operator-experience-squad + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // @grafana/grafana-operator-experience-squad go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel v1.29.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.30.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.29.0 // @grafana/grafana-backend-group - go.opentelemetry.io/otel/trace v1.29.0 // @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.30.0 // @grafana/grafana-backend-group go.uber.org/atomic v1.11.0 // @grafana/alerting-backend go.uber.org/goleak v1.3.0 // @grafana/grafana-search-and-storage gocloud.dev v0.39.0 // @grafana/grafana-app-platform-squad @@ -225,7 +225,7 @@ require ( github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect - github.com/RoaringBitmap/roaring v0.9.4 // indirect + github.com/RoaringBitmap/roaring v1.9.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect @@ -239,12 +239,12 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.2.0 // indirect + github.com/bits-and-blooms/bitset v1.12.0 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/segment v0.9.0 // indirect + github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect - github.com/blevesearch/vellum v1.0.7 // indirect + github.com/blevesearch/vellum v1.0.10 // indirect github.com/blugelabs/ice v1.0.0 // indirect github.com/bufbuild/protocompile v0.4.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect @@ -393,7 +393,7 @@ require ( github.com/redis/rueidis v1.0.45 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/cors v1.10.1 // @grafana/identity-access-team + github.com/rs/cors v1.11.1 // @grafana/identity-access-team github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -430,8 +430,8 @@ require ( go.etcd.io/etcd/client/v3 v3.5.14 // indirect go.mongodb.org/mongo-driver v1.16.1 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -498,7 +498,7 @@ replace github.com/crewjam/saml => github.com/grafana/saml v0.4.15-0.20240523142 // Use our fork of the upstream alertmanagers. // This is required in order to get notification delivery errors from the receivers API. -replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible diff --git a/go.sum b/go.sum index 143299e44c6..45fa6bd2fce 100644 --- a/go.sum +++ b/go.sum @@ -1466,8 +1466,8 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMz github.com/RoaringBitmap/gocroaring v0.4.0/go.mod h1:NieMwz7ZqwU2DD73/vvYwv7r4eWBKuPVSXZIpsaMwCI= github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76/go.mod h1:oM0MHmQ3nDsq609SS36p+oYbRi16+oVvU2Bw4Ipv0SE= github.com/RoaringBitmap/roaring v0.9.1/go.mod h1:h1B7iIUOmnAeb5ytYMvnHJwxMc6LUrwBnzXWRuqTQUc= -github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo= -github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= +github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM= +github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= @@ -1603,8 +1603,8 @@ github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xW github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= -github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= -github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= +github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI= +github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -1614,23 +1614,24 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= -github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= +github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= +github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/vellum v1.0.5/go.mod h1:atE0EH3fvk43zzS7t1YNdNC7DbmcC3uz+eMD5xZ2OyQ= -github.com/blevesearch/vellum v1.0.7 h1:+vn8rfyCRHxKVRgDLeR0FAXej2+6mEb5Q15aQE/XESQ= -github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= +github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI= +github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k= github.com/blugelabs/bluge v0.1.9 h1:bPgXlcsWugrXNjzeoLdOnvfJpHsyODKpYaAndayl/SM= github.com/blugelabs/bluge v0.1.9/go.mod h1:5d7LktUkQgvbh5Bmi6tPWtvo4+6uRTm6gAwP+5z6FqQ= github.com/blugelabs/bluge_segment_api v0.2.0 h1:cCX1Y2y8v0LZ7+EEJ6gH7dW6TtVTW4RhG0vp3R+N2Lo= @@ -2256,8 +2257,8 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20240926144415-27f4e81b4b6b h1:UO4mv94pG1kzKCgBKh20TXdACBCAK2vYjV3Q2MlcpEQ= -github.com/grafana/alerting v0.0.0-20240926144415-27f4e81b4b6b/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= +github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e h1:RttFYx5+RTNuMPlaftx8i9f91kwUi9LdxsoPLHnticU= +github.com/grafana/alerting v0.0.0-20240930154843-22cee00b280e/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20240919120951-58259833c564 h1:zYF/RBulpvMqPYR3gbzJZ8t/j/Eymn5FNidSYkueNCA= github.com/grafana/authlib v0.0.0-20240919120951-58259833c564/go.mod h1:PFzXbCrn0GIpN4KwT6NP1l5Z1CPLfmKHnYx8rZzQcyY= github.com/grafana/authlib/claims v0.0.0-20240827210201-19d5347dd8dd h1:sIlR7n38/MnZvX2qxDEszywXdI5soCwQ78aTDSARvus= @@ -2282,8 +2283,8 @@ github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e h1:UlEET0InuoFa github.com/grafana/gomemcache v0.0.0-20240805133030-fdaf6a95408e/go.mod h1:IGRj8oOoxwJbHBYl1+OhS9UjQR0dv6SQOep7HqmtyFU= github.com/grafana/grafana-app-sdk v0.19.0 h1:RY9HvCFR+4WUy81n53wejZnof9qE6++pd6r24d6+JYs= github.com/grafana/grafana-app-sdk v0.19.0/go.mod h1:y0BgzYxc+a7CwOqkwUhN9zXd5cgZJjd2zAbgHEd/xzo= -github.com/grafana/grafana-aws-sdk v0.31.2 h1:Mv01GAHcIG3S2pVtRlt1cUnnWzUAr4qr74HJvYv11JQ= -github.com/grafana/grafana-aws-sdk v0.31.2/go.mod h1:5nt5Gmp6+GyM+Jr7xsXKJtbizxbYXXLmEac6kw5paQI= +github.com/grafana/grafana-aws-sdk v0.31.3 h1:QlgIwyyozYYQf/dL279Baicyax+SuE99Set5chMnq1s= +github.com/grafana/grafana-aws-sdk v0.31.3/go.mod h1:5nt5Gmp6+GyM+Jr7xsXKJtbizxbYXXLmEac6kw5paQI= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 h1:fV6IgVtViXcYZ4VqTAMuVBTLuGAnI27HhQkaLttzbPE= github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0= github.com/grafana/grafana-cloud-migration-snapshot v1.3.0 h1:F0O9eTy4jHjEd1Z3/qIza2GdY7PYpTddUeaq9p3NKGU= @@ -2293,8 +2294,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQWUh6L85c6EJUN8Jdo= -github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= +github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990 h1:uQMZE/z+Y+o/U0z/g8ckAHss7U7LswedilByA2535DU= github.com/grafana/grafana/apps/playlist v0.0.0-20240917082838-e2bce38a7990/go.mod h1:3Vi0xv/4OBkBw4R9GAERkSrBnx06qrjpmNBRisucuSM= github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI= @@ -2315,8 +2316,8 @@ github.com/grafana/grafana/pkg/util/xorm v0.0.1 h1:72QZjxWIWpSeOF8ob4aMV058kfgZy github.com/grafana/grafana/pkg/util/xorm v0.0.1/go.mod h1:eNfbB9f2jM8o9RfwqwjY8SYm5tvowJ8Ly+iE4P9rXII= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 h1:AJKOtDKAOg8XNFnIZSmqqqutoTSxVlRs6vekL2p2KEY= -github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 h1:6D2gGAwyQBElSrp3E+9lSr7k8gLuP3Aiy20rweLWeBw= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3/go.mod h1:YeND+6FDA7OuFgDzYODN8kfPhXLCehcpxe4T9mdnpCY= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= github.com/grafana/pyroscope-go/godeltaprof v0.1.8/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/grafana/pyroscope/api v0.3.0 h1:WcVKNZ8JlriJnD28wTkZray0wGo8dGkizSJXnbG7Gd8= @@ -3055,8 +3056,9 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= @@ -3341,14 +3343,14 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 h1:IVtyPth4Rs5P8wIf0mP2KVKFNTJ4paX9qQ4Hkh5gFdc= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.50.0/go.mod h1:DKdbWcT4GH1D0Y3Sqt/PFXt2naRKDWtU+eE6oLdFNA8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 h1:+YPiqF5rR6PqHBlmEFLPumbSP0gY0WmCGFayXRcCLvs= go.opentelemetry.io/contrib/propagators/jaeger v1.29.0/go.mod h1:6PD7q7qquWSp3Z4HeM3e/2ipRubaY1rXZO8NIHVDZjs= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 h1:qKi9ntCcronqWqfuKxqrxZlZd82jXJEgGiAWH1+phxo= @@ -3360,8 +3362,8 @@ go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZV go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= @@ -3378,8 +3380,8 @@ go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xC go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= @@ -3393,8 +3395,8 @@ go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40 go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= diff --git a/go.work b/go.work index 846ebb7016d..e75f9e25b4e 100644 --- a/go.work +++ b/go.work @@ -23,3 +23,5 @@ use ( replace xorm.io/xorm => ./pkg/util/xorm replace github.com/getkin/kin-openapi => github.com/getkin/kin-openapi v0.125.0 + +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240930132144-b5e64e81e8d3 diff --git a/go.work.sum b/go.work.sum index ca6f13d695d..c0989dbc926 100644 --- a/go.work.sum +++ b/go.work.sum @@ -308,6 +308,8 @@ github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJs github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0= github.com/RoaringBitmap/gocroaring v0.4.0 h1:5nufXUgWpBEUNEJXw7926YAA58ZAQRpWPrQV1xCoSjc= github.com/RoaringBitmap/real-roaring-datasets v0.0.0-20190726190000-eb7c87156f76 h1:ZYlhPbqQFU+AHfgtCdHGDTtRW1a8geZyiE8c6Q+Sl1s= +github.com/RoaringBitmap/roaring v0.9.4 h1:ckvZSX5gwCRaJYBNe7syNawCU5oruY9gQmjXlp4riwo= +github.com/RoaringBitmap/roaring v0.9.4/go.mod h1:icnadbWcNyfEHlYdr+tDlOTih1Bf/h+rzPpv4sbomAA= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0= github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= github.com/Shopify/sarama v1.19.0 h1:9oksLxC6uxVPHPVYUmq6xhr1BOF/hHobWH2UzO67z1s= @@ -333,6 +335,8 @@ github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= github.com/alicebob/miniredis v2.5.0+incompatible h1:yBHoLpsyjupjz3NL3MhKMVkR41j82Yjf3KFv7ApYzUI= github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= @@ -368,6 +372,19 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= +github.com/bits-and-blooms/bitset v1.2.0 h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA= +github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA= +github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A= +github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI= +github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ= +github.com/blevesearch/mmap-go v1.0.3/go.mod h1:pYvKl/grLQrBxuaRYgoTssa4rVujYYeenDp++2E+yvs= +github.com/blevesearch/segment v0.9.0 h1:5lG7yBCx98or7gK2cHMKPukPZ/31Kag7nONpoBt22Ac= +github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A= +github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg= +github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc= +github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc= +github.com/blevesearch/vellum v1.0.7 h1:+vn8rfyCRHxKVRgDLeR0FAXej2+6mEb5Q15aQE/XESQ= +github.com/blevesearch/vellum v1.0.7/go.mod h1:doBZpmRhwTsASB4QdUZANlJvqVAUdUyX0ZK7QJCTeBE= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= @@ -417,6 +434,10 @@ github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjs github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf h1:CAKfRE2YtTUIjjh1bkBtyYFaUT/WmOqsJjgtihT0vMI= +github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps= +github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= +github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o= +github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -470,8 +491,12 @@ github.com/elastic/go-sysinfo v1.11.2/go.mod h1:GKqR8bbMK/1ITnez9NIsIfXQr25aLhRJ github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elazarl/goproxy v0.0.0-20230731152917-f99041a5c027/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw= github.com/expr-lang/expr v1.16.2 h1:JvMnzUs3LeVHBvGFcXYmXo+Q6DPDmzrlcSBO6Wy3w4s= github.com/expr-lang/expr v1.16.2/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= @@ -525,6 +550,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198 h1:FSii2UQeSLngl3jFoR4tUKZLprO7qUlh/TKKticc0BM= github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4 h1:vF83LI8tAakwEwvWZtrIEx7pOySacl2TOxx6eXk4ePo= github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= @@ -563,7 +590,13 @@ github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18 h1:3cQ+d+fkNL2Eqp github.com/grafana/alerting v0.0.0-20240830172655-aa466962ea18/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10 h1:oDbLKM34O+JUF9EQFS+9aYhdYoeNfUpXqNjFCLIxwF4= github.com/grafana/alerting v0.0.0-20240917171353-6c25eb6eff10/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= +github.com/grafana/alerting v0.0.0-20240926233713-446ddd356f8d h1:HOK6RWTuVldWFtNbWHxPlTa2shZ+WsNJsxoRJhX56Zg= +github.com/grafana/alerting v0.0.0-20240926233713-446ddd356f8d/go.mod h1:GMLi6d09Xqo96fCVUjNk//rcjP5NKEdjOzfWIffD5r4= github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56/go.mod h1:PGk3RjYHpxMM8HFPhKKo+vve3DdlPUELZLSDEFehPuU= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45 h1:AJKOtDKAOg8XNFnIZSmqqqutoTSxVlRs6vekL2p2KEY= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240625192351-66ec17e3aa45/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= +github.com/grafana/grafana-aws-sdk v0.31.3 h1:QlgIwyyozYYQf/dL279Baicyax+SuE99Set5chMnq1s= +github.com/grafana/grafana-aws-sdk v0.31.3/go.mod h1:5nt5Gmp6+GyM+Jr7xsXKJtbizxbYXXLmEac6kw5paQI= github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26 h1:HX927q4X1n451pnGb8U0wq74i8PCzuxVjzv7TyD10kc= github.com/grafana/thema v0.0.0-20230511182720-3146087fcc26/go.mod h1:Pn9nfzCk7nV0mvNgwusgCjCROZP6nm4GpwTnmEhLT24= @@ -642,6 +675,8 @@ github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8= @@ -675,6 +710,8 @@ github.com/matryer/moq v0.3.3 h1:pScMH9VyrdT4S93yiLpVyU8rCDqGQr24uOyBxmktG5Q= github.com/matryer/moq v0.3.3/go.mod h1:RJ75ZZZD71hejp39j4crZLsEDszGk6iH4v4YsWFKH4s= github.com/matryer/moq v0.5.0 h1:h2PJUYjZSiyEahzVogDRmrgL9Bsx9xYAl8l+LPfmwL8= github.com/matryer/moq v0.5.0/go.mod h1:39GTnrD0mVWHPvWdYj5ki/lxfhLQEtHcLh+tWoYF/iE= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= @@ -792,6 +829,8 @@ github.com/relvacode/iso8601 v1.4.0 h1:GsInVSEJfkYuirYFxa80nMLbH2aydgZpIf52gYZXU github.com/relvacode/iso8601 v1.4.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245 h1:K1Xf3bKttbF+koVGaX5xngRIZ5bVjbmPnaxE/dR08uY= @@ -802,6 +841,8 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCL github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/scottlepp/go-duck v0.3.1 h1:UDm7AepGq7urXUAtF/Nxbrth6Te+k42pz/6jdpC3gp8= +github.com/scottlepp/go-duck v0.3.1/go.mod h1:cQRP0clf3eX6QUizCDTNMqShtW8Ek7ovnURCGOrJgyU= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e h1:uO75wNGioszjmIzcY/tvdDYKRLVvzggtAmmJkn9j4GQ= github.com/segmentio/fasthash v0.0.0-20180216231524-a72b379d632e/go.mod h1:tm/wZFQ8e24NYaBGIlnO2WGCAi67re4HHuOm0sftE/M= github.com/sercand/kuberesolver/v5 v5.1.1 h1:CYH+d67G0sGBj7q5wLK61yzqJJ8gLLC8aeprPTHb6yY= @@ -889,6 +930,8 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= go.einride.tech/aip v0.67.1 h1:d/4TW92OxXBngkSOwWS2CH5rez869KpKMaN44mdxkFI= go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI= +go.etcd.io/gofail v0.1.0 h1:XItAMIhOojXFQMgrxjnd2EIIHun/d5qL0Pf7FzVTkFg= +go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= go.opentelemetry.io/collector v0.97.0 h1:qyOju13byHIKEK/JehmTiGMj4pFLa4kDyrOCtTmjHU0= go.opentelemetry.io/collector v0.97.0/go.mod h1:V6xquYAaO2VHVu4DBK28JYuikRdZajh7DH5Vl/Y8NiA= go.opentelemetry.io/collector/component v0.97.0 h1:vanKhXl5nptN8igRH4PqVYHOILif653vaPIKv6LCZCI= @@ -995,11 +1038,9 @@ golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190921015927-1a5e07d1ff72/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1011,7 +1052,6 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -1023,14 +1063,12 @@ gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPj gonum.org/v1/plot v0.10.1 h1:dnifSs43YJuNMDzB7v8wV64O4ABBHReuAVAoBxqBqS4= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= 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-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys= google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:OFMYQFHJ4TM3JRlWDZhJbZfra2uqc3WLBZiaaqP4DtU= google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf h1:T4tsZBlZYXK3j40sQNP5MBO32I+rn6ypV1PpklsiV8k= google.golang.org/genproto/googleapis/bytestream v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:5/MT647Cn/GGhwTpXC7QqcaR5Cnee4v4MKCU1/nwnIQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= 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-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= diff --git a/package.json b/package.json index e2b7a7751f6..2550f233202 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "e2e:enterprise:dev": "./e2e/start-and-run-suite enterprise dev", "e2e:enterprise:debug": "./e2e/start-and-run-suite enterprise debug", "e2e:playwright": "yarn playwright test", - "e2e:playwright:server": "./e2e/plugin-e2e/start-and-run-suite", + "e2e:playwright:server": "yarn e2e:plugin:build && ./e2e/plugin-e2e/start-and-run-suite", "e2e:storybook": "PORT=9001 ./e2e/run-suite storybook true", "e2e:plugin:build": "nx run-many -t build --projects='@test-plugins/*'", "e2e:plugin:build:dev": "nx run-many -t dev --projects='@test-plugins/*' --maxParallel=100", @@ -76,10 +76,10 @@ "@betterer/cli": "5.4.0", "@betterer/eslint": "5.4.0", "@cypress/webpack-preprocessor": "6.0.2", - "@emotion/eslint-plugin": "11.11.0", + "@emotion/eslint-plugin": "11.12.0", "@grafana/eslint-config": "7.0.0", "@grafana/eslint-plugin": "link:./packages/grafana-eslint-rules", - "@grafana/plugin-e2e": "1.8.1", + "@grafana/plugin-e2e": "^1.8.3", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", "@playwright/test": "1.47.2", @@ -181,7 +181,7 @@ "eslint-plugin-jsx-a11y": "6.10.0", "eslint-plugin-lodash": "7.4.0", "eslint-plugin-no-barrel-files": "^1.1.0", - "eslint-plugin-react": "7.36.1", + "eslint-plugin-react": "7.37.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-testing-library": "^6.2.2", "eslint-scope": "^8.0.0", @@ -209,7 +209,7 @@ "mutationobserver-shim": "0.3.7", "ngtemplate-loader": "2.1.0", "node-notifier": "10.0.1", - "nx": "19.8.0", + "nx": "19.8.2", "postcss": "8.4.47", "postcss-loader": "8.1.1", "postcss-reporter": "7.1.0", @@ -223,7 +223,7 @@ "rimraf": "6.0.1", "rudder-sdk-js": "2.48.18", "sass": "1.79.3", - "sass-loader": "14.2.1", + "sass-loader": "16.0.2", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", "stylelint": "16.9.0", @@ -238,7 +238,7 @@ "webpack-assets-manifest": "^5.1.0", "webpack-bundle-analyzer": "4.10.2", "webpack-cli": "5.1.4", - "webpack-dev-server": "5.0.4", + "webpack-dev-server": "5.1.0", "webpack-manifest-plugin": "5.0.0", "webpack-merge": "5.10.0", "webpackbar": "^6.0.0", @@ -246,8 +246,8 @@ "yargs": "^17.5.1" }, "dependencies": { - "@emotion/css": "11.11.2", - "@emotion/react": "11.11.4", + "@emotion/css": "11.13.0", + "@emotion/react": "11.13.3", "@fingerprintjs/fingerprintjs": "^3.4.2", "@floating-ui/react": "0.26.24", "@formatjs/intl-durationformat": "^0.2.4", @@ -268,7 +268,7 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "5.16.0", + "@grafana/scenes": "5.16.2", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", @@ -406,7 +406,7 @@ "tslib": "2.7.0", "tween-functions": "^1.2.0", "type-fest": "^4.18.2", - "uplot": "1.6.30", + "uplot": "1.6.31", "uuid": "9.0.1", "visjs-network": "4.25.0", "whatwg-fetch": "3.6.20", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 45378752c73..7ff2bde3e22 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -28,7 +28,7 @@ "LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -57,7 +57,7 @@ "string-hash": "^1.1.3", "tinycolor2": "1.6.0", "tslib": "2.7.0", - "uplot": "1.6.30", + "uplot": "1.6.31", "xss": "^1.0.14" }, "devDependencies": { @@ -75,10 +75,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "typescript": "5.5.4" }, "peerDependencies": { diff --git a/packages/grafana-data/rollup.config.ts b/packages/grafana-data/rollup.config.ts index 1ac767eddcf..9894024f99c 100644 --- a/packages/grafana-data/rollup.config.ts +++ b/packages/grafana-data/rollup.config.ts @@ -1,20 +1,35 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); + +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -23,6 +38,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-data/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-data/src/field/overrides/processors.ts b/packages/grafana-data/src/field/overrides/processors.ts index 9846c1c3ef3..8c8542fd6ff 100644 --- a/packages/grafana-data/src/field/overrides/processors.ts +++ b/packages/grafana-data/src/field/overrides/processors.ts @@ -1,3 +1,4 @@ +import { Action } from '../../types/action'; import { Field } from '../../types/dataFrame'; import { DataLink } from '../../types/dataLink'; import { FieldOverrideContext } from '../../types/fieldOverrides'; @@ -59,6 +60,15 @@ export const dataLinksOverrideProcessor = ( return value; }; +export const actionsOverrideProcessor = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any, + _context: FieldOverrideContext, + _settings?: DataLinksFieldConfigSettings +): Action[] => { + return value; +}; + export interface ValueMappingFieldConfigSettings {} export const valueMappingsOverrideProcessor = ( diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index 291f88b2333..4a0f8adbe58 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -1,6 +1,7 @@ import { HideSeriesConfig } from '@grafana/schema'; import { ScopedVars } from './ScopedVars'; +import { Action } from './action'; import { QueryResultBase, Labels, NullValueMode } from './data'; import { DataLink, LinkModel } from './dataLink'; import { DecimalCount, DisplayProcessor, DisplayValue, DisplayValueAlignmentFactors } from './displayValue'; @@ -98,6 +99,8 @@ export interface FieldConfig { // The behavior when clicking on a result links?: DataLink[]; + actions?: Action[]; + // Alternative to empty string noValue?: string; diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7104c77c50e..f01cb7197a0 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -117,6 +117,7 @@ export interface FeatureToggles { kubernetesSnapshots?: boolean; kubernetesDashboards?: boolean; kubernetesFolders?: boolean; + grafanaAPIServerTestingWithExperimentalAPIs?: boolean; datasourceQueryTypes?: boolean; queryService?: boolean; queryServiceRewrite?: boolean; @@ -204,6 +205,7 @@ export interface FeatureToggles { dataplaneAggregator?: boolean; newFiltersUI?: boolean; lokiSendDashboardPanelNames?: boolean; + alertingPrometheusRulesPrimary?: boolean; singleTopNav?: boolean; exploreLogsShardSplitting?: boolean; exploreLogsAggregatedMetrics?: boolean; @@ -216,4 +218,5 @@ export interface FeatureToggles { improvedExternalSessionHandling?: boolean; useSessionStorageForRedirection?: boolean; rolePickerDrawer?: boolean; + unifiedStorageSearch?: boolean; } diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 022a12f9c4a..73973dd4318 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -31,8 +31,8 @@ "LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -43,10 +43,10 @@ "@types/node": "20.16.9", "esbuild": "0.24.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0" + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3" }, "dependencies": { "@grafana/tsconfig": "^2.0.0", diff --git a/packages/grafana-e2e-selectors/rollup.config.ts b/packages/grafana-e2e-selectors/rollup.config.ts index bf8b4ccd1eb..b70662c33a8 100644 --- a/packages/grafana-e2e-selectors/rollup.config.ts +++ b/packages/grafana-e2e-selectors/rollup.config.ts @@ -1,20 +1,35 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); + +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -23,6 +38,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-e2e-selectors/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 0849c031faf..29f945b8d28 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -31,8 +31,8 @@ "./LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -43,7 +43,7 @@ "not IE 11" ], "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@grafana/data": "11.3.0-pre", "@grafana/ui": "11.3.0-pre", "@leeoniya/ufuzzy": "1.0.14", @@ -76,10 +76,10 @@ "esbuild": "0.24.0", "jest": "^29.6.4", "jest-canvas-mock": "2.5.2", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "ts-jest": "29.2.5", "ts-node": "10.9.2", "typescript": "5.5.4" diff --git a/packages/grafana-flamegraph/rollup.config.ts b/packages/grafana-flamegraph/rollup.config.ts index 0dd3c9f2f65..86d0ad86cfa 100644 --- a/packages/grafana-flamegraph/rollup.config.ts +++ b/packages/grafana-flamegraph/rollup.config.ts @@ -1,20 +1,35 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); + +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -23,6 +38,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-ui/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index 056688c86d7..98cb0c4bb0d 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -31,13 +31,13 @@ "typecheck": "yarn generate && tsc --emitDeclarationOnly false --noEmit", "lint": "eslint --ext .ts,.tsx ./src", "prettier:check": "prettier --check --list-different=false --log-level=warn \"**/*.{ts,tsx,scss,md,mdx,json}\"", - "build": "yarn generate && rollup -c rollup.config.ts" + "build": "yarn generate && rollup -c rollup.config.ts --configPlugin esbuild" }, "devDependencies": { "@babel/core": "7.25.2", "@grafana/tsconfig": "^2.0.0", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-typescript": "^11.1.6", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-typescript": "^12.1.0", "@svgr/babel-plugin-remove-jsx-attribute": "^8.0.0", "@svgr/cli": "^8.1.0", "@svgr/core": "8.1.0", @@ -53,10 +53,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^6.1.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "7.1.3", "ts-node": "10.9.2", "typescript": "5.5.4" }, diff --git a/packages/grafana-icons/rollup.config.ts b/packages/grafana-icons/rollup.config.ts index cfbe94f8ac4..6bf3c3b536e 100644 --- a/packages/grafana-icons/rollup.config.ts +++ b/packages/grafana-icons/rollup.config.ts @@ -1,21 +1,36 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import externals from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -import pkg from './package.json'; +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); + +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'esm', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), preserveModules: true, + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index f6c5c6cb8ca..df4acb26060 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -17,7 +17,7 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit" }, "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@grafana/data": "11.3.0-pre", "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/experimental": "1.8.0", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index ad5afa4225b..08c7f213e9f 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -28,15 +28,15 @@ "access": "public" }, "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", "postpack": "mv package.json.bak package.json" }, "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@floating-ui/react": "0.26.24", "@grafana/data": "11.3.0-pre", "@grafana/experimental": "1.8.0", @@ -75,7 +75,7 @@ "whatwg-fetch": "3.6.20" }, "devDependencies": { - "@emotion/eslint-plugin": "11.11.0", + "@emotion/eslint-plugin": "11.12.0", "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-image": "3.0.3", @@ -114,7 +114,7 @@ "eslint-plugin-jsdoc": "48.11.0", "eslint-plugin-jsx-a11y": "6.10.0", "eslint-plugin-lodash": "7.4.0", - "eslint-plugin-react": "7.36.1", + "eslint-plugin-react": "7.37.0", "eslint-plugin-react-hooks": "4.6.0", "eslint-webpack-plugin": "4.2.0", "fork-ts-checker-webpack-plugin": "9.0.2", @@ -127,12 +127,12 @@ "react-dom": "18.2.0", "react-select-event": "5.5.1", "react-test-renderer": "18.2.0", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "sass": "1.79.3", - "sass-loader": "14.2.1", + "sass-loader": "16.0.2", "style-loader": "4.0.0", "testing-library-selector": "0.3.1", "ts-node": "10.9.2", diff --git a/packages/grafana-prometheus/rollup.config.ts b/packages/grafana-prometheus/rollup.config.ts index 080514c27d5..7e630257c36 100644 --- a/packages/grafana-prometheus/rollup.config.ts +++ b/packages/grafana-prometheus/rollup.config.ts @@ -1,21 +1,37 @@ import image from '@rollup/plugin-image'; import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); + +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild(), image()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + image(), + ], output: [ { format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -24,6 +40,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-prometheus/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index cb416fa8f0c..29b53f51902 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -74,7 +74,13 @@ import { import { PrometheusVariableSupport } from './variables'; const ANNOTATION_QUERY_STEP_DEFAULT = '60s'; -const GET_AND_POST_METADATA_ENDPOINTS = ['api/v1/query', 'api/v1/query_range', 'api/v1/series', 'api/v1/labels']; +const GET_AND_POST_METADATA_ENDPOINTS = [ + 'api/v1/query', + 'api/v1/query_range', + 'api/v1/series', + 'api/v1/labels', + 'suggestions', +]; export const InstantQueryRefIdIndex = '-Instant'; @@ -263,7 +269,9 @@ export class PrometheusDatasource .join('&'); } } else { - options.headers!['Content-Type'] = 'application/x-www-form-urlencoded'; + if (!options.headers!['Content-Type']) { + options.headers!['Content-Type'] = 'application/x-www-form-urlencoded'; + } options.data = data; } @@ -441,8 +449,7 @@ export class PrometheusDatasource } const scopedVars = { - __interval: { text: this.interval, value: this.interval }, - __interval_ms: { text: rangeUtil.intervalToMs(this.interval), value: rangeUtil.intervalToMs(this.interval) }, + ...this.getIntervalVars(), ...this.getRangeScopedVars(options?.range ?? getDefaultTimeRange()), }; const interpolated = this.templateSrv.replace(query, scopedVars, this.interpolateQueryExpr); @@ -450,6 +457,13 @@ export class PrometheusDatasource return metricFindQuery.process(options?.range ?? getDefaultTimeRange()); } + getIntervalVars() { + return { + __interval: { text: this.interval, value: this.interval }, + __interval_ms: { text: rangeUtil.intervalToMs(this.interval), value: rangeUtil.intervalToMs(this.interval) }, + }; + } + getRangeScopedVars(range: TimeRange) { const msRange = range.to.diff(range.from); const sRange = Math.round(msRange / 1000); @@ -599,6 +613,20 @@ export class PrometheusDatasource // it is used in metric_find_query.ts // and in Tempo here grafana/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx async getTagKeys(options: DataSourceGetTagKeysOptions): Promise { + if (config.featureToggles.promQLScope && !!options) { + const suggestions = await this.languageProvider.fetchSuggestions( + options.timeRange, + options.queries, + options.scopes, + options.filters + ); + + // filter out already used labels and empty labels + return suggestions + .filter((labelName) => !!labelName && !options.filters.find((filter) => filter.key === labelName)) + .map((k) => ({ value: k, text: k })); + } + if (!options || options.filters.length === 0) { await this.languageProvider.fetchLabels(options.timeRange, options.queries); return this.languageProvider.getLabelKeys().map((k) => ({ value: k, text: k })); @@ -621,6 +649,21 @@ export class PrometheusDatasource // By implementing getTagKeys and getTagValues we add ad-hoc filters functionality async getTagValues(options: DataSourceGetTagValuesOptions) { + const requestId = `[${this.uid}][${options.key}]`; + if (config.featureToggles.promQLScope) { + return ( + await this.languageProvider.fetchSuggestions( + options.timeRange, + options.queries, + options.scopes, + options.filters, + options.key, + undefined, + requestId + ) + ).map((v) => ({ value: v, text: v })); + } + const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({ label: f.key, value: f.value, @@ -630,7 +673,6 @@ export class PrometheusDatasource const expr = promQueryModeller.renderLabels(labelFilters); if (this.hasLabelsMatchAPISupport()) { - const requestId = `[${this.uid}][${options.key}]`; return ( await this.languageProvider.fetchSeriesValuesWithMatch(options.key, expr, requestId, options.timeRange) ).map((v) => ({ diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 102ec109a90..105c354dcae 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -6,8 +6,12 @@ import { AbstractLabelMatcher, AbstractLabelOperator, AbstractQuery, + AdHocVariableFilter, getDefaultTimeRange, LanguageProvider, + Scope, + scopeFilterOperatorMap, + ScopeSpecFilter, TimeRange, } from '@grafana/data'; import { BackendSrvRequest } from '@grafana/runtime'; @@ -407,6 +411,69 @@ export default class PromQlLanguageProvider extends LanguageProvider { const values = await Promise.all(DEFAULT_KEYS.map((key) => this.fetchLabelValues(key))); return DEFAULT_KEYS.reduce((acc, key, i) => ({ ...acc, [key]: values[i] }), {}); }); + + /** + * Fetch labels or values for a label based on the queries, scopes, filters and time range + * @param timeRange + * @param queries + * @param scopes + * @param adhocFilters + * @param labelName + * @param limit + * @param requestId + */ + fetchSuggestions = async ( + timeRange?: TimeRange, + queries?: PromQuery[], + scopes?: Scope[], + adhocFilters?: AdHocVariableFilter[], + labelName?: string, + limit?: number, + requestId?: string + ): Promise => { + if (timeRange) { + this.timeRange = timeRange; + } + + const url = '/suggestions'; + const timeParams = this.datasource.getAdjustedInterval(this.timeRange); + const value = await this.request( + url, + [], + { + labelName, + queries: queries?.map((q) => + this.datasource.interpolateString(q.expr, { + ...this.datasource.getIntervalVars(), + ...this.datasource.getRangeScopedVars(this.timeRange), + }) + ), + scopes: scopes?.reduce((acc, scope) => { + acc.push(...scope.spec.filters); + + return acc; + }, []), + adhocFilters: adhocFilters?.map((filter) => ({ + key: filter.key, + operator: scopeFilterOperatorMap[filter.operator], + value: filter.value, + values: filter.values, + })), + limit, + ...timeParams, + }, + { + ...(requestId && { requestId }), + headers: { + ...this.getDefaultCacheHeaders()?.headers, + 'Content-Type': 'application/json', + }, + method: 'POST', + } + ); + + return value ?? []; + }; } function getNameLabelValue(promQuery: string, tokens: Array): string { diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts index 01cd25fd14f..28e10381b61 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.test.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.test.ts @@ -7,7 +7,11 @@ import { QueryEditorMode } from '../querybuilder/shared/types'; import { PromQuery } from '../types'; import { CacheRequestInfo, findDatapointStep, QueryCache } from './QueryCache'; -import { IncrementalStorageDataFrameScenarios, trimmedFirstPointInPromFrames } from './QueryCacheTestData'; +import { + differentDisplayNameFromDS, + trimmedFirstPointInPromFrames, + IncrementalStorageDataFrameScenarios, +} from './QueryCacheTestData'; // Will not interpolate vars! const interpolateStringTest = (query: PromQuery) => { @@ -124,7 +128,7 @@ describe('QueryCache: Generic', function () { }), { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }, firstFrames @@ -148,7 +152,7 @@ describe('QueryCache: Generic', function () { secondRequest, { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }, secondFrames @@ -241,7 +245,7 @@ describe('QueryCache: Prometheus', function () { request, { requests: [], // unused - targSigs: targetSignatures, + targetSignatures: targetSignatures, shouldCache: true, }, firstFrames @@ -265,7 +269,7 @@ describe('QueryCache: Prometheus', function () { secondRequest, { requests: [], // unused - targSigs: targetSignatures, + targetSignatures: targetSignatures, shouldCache: true, }, secondFrames @@ -387,9 +391,9 @@ describe('QueryCache: Prometheus', function () { panelId: panelId, }); - const requestInfo = { + const requestInfo: CacheRequestInfo = { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }; const targetSignature = `1=1|${interval}|${JSON.stringify(request.rangeRaw ?? '')}`; @@ -407,7 +411,7 @@ describe('QueryCache: Prometheus', function () { }), { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }, secondFrames @@ -430,7 +434,7 @@ describe('QueryCache: Prometheus', function () { }), { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }, thirdFrames @@ -537,7 +541,7 @@ describe('QueryCache: Prometheus', function () { request.targets[0].interval = '1m'; const requestInfo: CacheRequestInfo = { requests: [], // unused - targSigs: cache, + targetSignatures: cache, shouldCache: true, }; const targetSignature = `1=1|${interval}|${JSON.stringify(request.rangeRaw ?? '')}`; @@ -558,6 +562,87 @@ describe('QueryCache: Prometheus', function () { expect(cacheRequest.requests[0]).toBe(request); expect(cacheRequest.shouldCache).toBe(true); }); + + it('should always use the newest config information', () => { + const storage = new QueryCache({ + getTargetSignature: getPrometheusTargetSignature, + overlapString: '10m', + }); + + // Initial request with custom legend info {{org}}-customLegend + const firstFrames = differentDisplayNameFromDS.first.dataFrames as unknown as DataFrame[]; + + // Second request with legend __auto which results having no displayNameFromDS + const secondFrames = differentDisplayNameFromDS.second.dataFrames as unknown as DataFrame[]; + + const cache = new Map(); + const interval = 15000; + + // start time of scenario + const firstFrom = dateTime(new Date(1726829205000)); + const firstTo = dateTime(new Date(1726829832515)); + const firstRange: TimeRange = { + from: firstFrom, + to: firstTo, + raw: { + from: 'now-1h', + to: 'now', + }, + }; + + const secondFrom = dateTime(new Date(1726829220000)); + const secondTo = dateTime(new Date(1726829903931)); + const secondRange: TimeRange = { + from: secondFrom, + to: secondTo, + raw: { + from: 'now-1h', + to: 'now', + }, + }; + + // Signifier definition + const dashboardId = `dashid`; + const panelId = 200; + const targetIdentity = `${dashboardId}|${panelId}|A`; + + const request = mockPromRequest({ + range: firstRange, + dashboardUID: dashboardId, + panelId: panelId, + app: 'first_app', + }); + + const requestInfo: CacheRequestInfo = { + requests: [], // unused + targetSignatures: cache, + shouldCache: true, + }; + const targetSignature = `1=1|${interval}|${JSON.stringify(request.rangeRaw ?? '')}`; + cache.set(targetIdentity, targetSignature); + + const firstQueryResult = storage.procFrames(request, requestInfo, firstFrames); + + expect(firstQueryResult[0].fields[1].config.displayNameFromDS).toBeDefined(); + expect(firstQueryResult[0].fields[1].config.displayNameFromDS).toEqual('rutgerkerkhoffdevuseast-customLegend'); + + const secondQueryResult = storage.procFrames( + mockPromRequest({ + range: secondRange, + dashboardUID: dashboardId, + panelId: panelId, + app: 'second_app', + }), + { + requests: [], // unused + targetSignatures: cache, + shouldCache: true, + }, + secondFrames + ); + + expect(secondQueryResult[0].fields[1].config.displayNameFromDS).not.toBeDefined(); + }); }); describe('findDataPointStep', () => { diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.ts b/packages/grafana-prometheus/src/querycache/QueryCache.ts index 9b06746b7cb..3afc3344f20 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.ts @@ -23,7 +23,7 @@ type TargetIdent = string; // query + template variables + interval + raw time range // used for full target cache busting -> full range re-query -type TargetSig = string; +type TargetSignature = string; type TimestampMs = number; type SupportedQueryTypes = PromQuery; type ApplyInterpolation = (str: string, scopedVars?: ScopedVars) => string; @@ -32,14 +32,14 @@ type ApplyInterpolation = (str: string, scopedVars?: ScopedVars) => string; export const defaultPrometheusQueryOverlapWindow = '10m'; interface TargetCache { - sig: TargetSig; + signature: TargetSignature; prevTo: TimestampMs; frames: DataFrame[]; } export interface CacheRequestInfo { requests: Array>; - targSigs: Map; + targetSignatures: Map; shouldCache: boolean; } @@ -48,13 +48,13 @@ export interface CacheRequestInfo { * This is the string used to uniquely identify a field within a "target" * @param field */ -export const getFieldIdent = (field: Field) => `${field.type}|${field.name}|${JSON.stringify(field.labels ?? '')}`; +export const getFieldIdentity = (field: Field) => `${field.type}|${field.name}|${JSON.stringify(field.labels ?? '')}`; /** * NOMENCLATURE * Target: The request target (DataQueryRequest), i.e. a specific query reference within a panel - * Ident: Identity: the string that is not expected to change - * Sig: Signature: the string that is expected to change, upon which we wipe the cache fields + * Identity: the string that is not expected to change + * Signature: the string that is expected to change, upon which we wipe the cache fields */ export class QueryCache { private overlapWindowMs: number; @@ -97,20 +97,20 @@ export class QueryCache { let doPartialQuery = shouldCache; let prevTo: TimestampMs | undefined = undefined; - // pre-compute reqTargSigs - const reqTargSigs = new Map(); - request.targets.forEach((targ) => { - let targIdent = `${request.dashboardUID}|${request.panelId}|${targ.refId}`; - let targSig = this.getTargetSignature(request, targ); // ${request.maxDataPoints} ? - reqTargSigs.set(targIdent, targSig); + // pre-compute reqTargetSignatures + const reqTargetSignatures = new Map(); + request.targets.forEach((target) => { + let targetIdentity = `${request.dashboardUID}|${request.panelId}|${target.refId}`; + let targetSignature = this.getTargetSignature(request, target); // ${request.maxDataPoints} ? + reqTargetSignatures.set(targetIdentity, targetSignature); }); // figure out if new query range or new target props trigger full cache invalidation & re-query - for (const [targIdent, targSig] of reqTargSigs) { - let cached = this.cache.get(targIdent); - let cachedSig = cached?.sig; + for (const [targetIdentity, targetSignature] of reqTargetSignatures) { + let cached = this.cache.get(targetIdentity); + let cachedSig = cached?.signature; - if (cachedSig !== targSig) { + if (cachedSig !== targetSignature) { doPartialQuery = false; } else { // only do partial queries when new request range follows prior request range (possibly with overlap) @@ -142,14 +142,14 @@ export class QueryCache { }, }; } else { - reqTargSigs.forEach((targSig, targIdent) => { + reqTargetSignatures.forEach((targSig, targIdent) => { this.cache.delete(targIdent); }); } return { requests: [request], - targSigs: reqTargSigs, + targetSignatures: reqTargetSignatures, shouldCache, }; } @@ -168,13 +168,13 @@ export class QueryCache { const respByTarget = new Map(); respFrames.forEach((frame: DataFrame) => { - let targIdent = `${request.dashboardUID}|${request.panelId}|${frame.refId}`; + let targetIdent = `${request.dashboardUID}|${request.panelId}|${frame.refId}`; - let frames = respByTarget.get(targIdent); + let frames = respByTarget.get(targetIdent); if (!frames) { frames = []; - respByTarget.set(targIdent, frames); + respByTarget.set(targetIdent, frames); } frames.push(frame); @@ -182,8 +182,8 @@ export class QueryCache { let outFrames: DataFrame[] = []; - respByTarget.forEach((respFrames, targIdent) => { - let cachedFrames = (targIdent ? this.cache.get(targIdent)?.frames : null) ?? []; + respByTarget.forEach((respFrames, targetIdentity) => { + let cachedFrames = (targetIdentity ? this.cache.get(targetIdentity)?.frames : null) ?? []; respFrames.forEach((respFrame: DataFrame) => { // skip empty frames @@ -193,9 +193,9 @@ export class QueryCache { // frames are identified by their second (non-time) field's name + labels // TODO: maybe also frame.meta.type? - let respFrameIdent = getFieldIdent(respFrame.fields[1]); + let respFrameIdentity = getFieldIdentity(respFrame.fields[1]); - let cachedFrame = cachedFrames.find((cached) => getFieldIdent(cached.fields[1]) === respFrameIdent); + let cachedFrame = cachedFrames.find((cached) => getFieldIdentity(cached.fields[1]) === respFrameIdentity); if (!cachedFrame) { // append new unknown frames @@ -213,6 +213,9 @@ export class QueryCache { if (amendedTable) { for (let i = 0; i < amendedTable.length; i++) { cachedFrame.fields[i].values = amendedTable[i]; + if (cachedFrame.fields[i].config.displayNameFromDS !== respFrame.fields[i].config.displayNameFromDS) { + cachedFrame.fields[i].config.displayNameFromDS = respFrame.fields[i].config.displayNameFromDS; + } } cachedFrame.length = cachedFrame.fields[0].values.length; } @@ -239,8 +242,8 @@ export class QueryCache { } }); - this.cache.set(targIdent, { - sig: requestInfo.targSigs.get(targIdent)!, + this.cache.set(targetIdentity, { + signature: requestInfo.targetSignatures.get(targetIdentity)!, frames: nonEmptyCachedFrames, prevTo: newTo, }); diff --git a/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts b/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts index 91aca344f2f..ae283225f1b 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCacheTestData.ts @@ -1006,3 +1006,258 @@ export const IncrementalStorageDataFrameScenarios = { }, }, }; + +export const differentDisplayNameFromDS = { + first: { + dataFrames: [ + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + executedQueryString: + 'Expr: sum by (org, stackId)(count_over_time(hosted_grafana:grafana_datasource_loki_orgs_stacks_queries:count2m{org=~".*", stackId=~".*"}[5m]))\nStep: 5m0s', + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'rutgerkerkhoffdevuseast', + stackId: '2791', + }, + config: { + displayNameFromDS: 'rutgerkerkhoffdevuseast-customLegend', + }, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'securityops', + stackId: '1533', + }, + config: { + displayNameFromDS: 'securityops-customLegend', + }, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'stephaniehingtgen', + stackId: '3740', + }, + config: { + displayNameFromDS: 'stephaniehingtgen-customLegend', + }, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + ], + }, + second: { + dataFrames: [ + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + executedQueryString: + 'Expr: sum by (org, stackId)(count_over_time(hosted_grafana:grafana_datasource_loki_orgs_stacks_queries:count2m{org=~".*", stackId=~".*"}[5m]))\nStep: 5m0s', + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'rutgerkerkhoffdevuseast', + stackId: '2791', + }, + config: {}, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'securityops', + stackId: '1533', + }, + config: {}, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + { + refId: 'A', + meta: { + type: 'timeseries-multi', + typeVersion: [0, 1], + custom: { + resultType: 'matrix', + }, + }, + fields: [ + { + name: 'Time', + type: 'time', + typeInfo: { + frame: 'time.Time', + }, + config: { + interval: 300000, + }, + values: [1726829100000, 1726829400000, 1726829700000], + entities: {}, + }, + { + name: 'Value', + type: 'number', + typeInfo: { + frame: 'float64', + }, + labels: { + org: 'stephaniehingtgen', + stackId: '3740', + }, + config: {}, + values: [3, 2, 3], + entities: {}, + }, + ], + length: 3, + }, + ], + }, +}; diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index f773eb75175..a690d8d5395 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -29,8 +29,8 @@ "LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -66,10 +66,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "rollup-plugin-sourcemaps": "0.6.3", "typescript": "5.5.4" }, diff --git a/packages/grafana-runtime/rollup.config.ts b/packages/grafana-runtime/rollup.config.ts index 241d425641c..25992a93441 100644 --- a/packages/grafana-runtime/rollup.config.ts +++ b/packages/grafana-runtime/rollup.config.ts @@ -1,20 +1,35 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); + +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -23,6 +38,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-runtime/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 39c8d34ab20..6b0ca630ad3 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -28,8 +28,8 @@ "LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "typecheck": "tsc --emitDeclarationOnly false --noEmit", "prepack": "cp package.json package.json.bak && node ../../scripts/prepare-packagejson.js", @@ -41,10 +41,10 @@ "esbuild": "0.24.0", "glob": "^11.0.0", "rimraf": "6.0.1", - "rollup": "2.79.1", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", "typescript": "5.5.4" }, "dependencies": { diff --git a/packages/grafana-schema/rollup.config.ts b/packages/grafana-schema/rollup.config.ts index d75af54dbfc..6a2f736c944 100644 --- a/packages/grafana-schema/rollup.config.ts +++ b/packages/grafana-schema/rollup.config.ts @@ -1,22 +1,37 @@ import resolve from '@rollup/plugin-node-resolve'; -import glob from 'glob'; +import { glob } from 'glob'; +import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; import path from 'path'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const pkg = rq('./package.json'); + +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; export default [ { input: 'src/index.ts', - plugins: [externals({ deps: true, packagePath: './package.json' }), resolve(), esbuild()], + plugins: [ + nodeExternals({ deps: true, packagePath: './package.json' }), + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: [ { format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -25,6 +40,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-schema/src`), + ...legacyOutputDefaults, }, ], }, @@ -45,7 +61,13 @@ export default [ fileURLToPath(new URL(file, import.meta.url)), ]) ), - plugins: [resolve(), esbuild()], + plugins: [ + resolve(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), + ], output: { format: 'esm', dir: path.dirname(pkg.publishConfig.module), diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index 409e725d56e..854d02f8ac6 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -14,7 +14,7 @@ "typecheck": "tsc --emitDeclarationOnly false --noEmit" }, "dependencies": { - "@emotion/css": "11.11.2", + "@emotion/css": "11.13.0", "@grafana/data": "11.3.0-pre", "@grafana/e2e-selectors": "11.3.0-pre", "@grafana/experimental": "1.8.0", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 3c82160a6fc..b44d5cd61a9 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -32,8 +32,8 @@ "./LICENSE_APACHE2" ], "scripts": { - "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts", - "bundle": "rollup -c rollup.config.ts", + "build": "tsc -p ./tsconfig.build.json && rollup -c rollup.config.ts --configPlugin esbuild", + "bundle": "rollup -c rollup.config.ts --configPlugin esbuild", "clean": "rimraf ./dist ./compiled ./package.tgz", "storybook": "storybook dev -p 9001 -c .storybook --no-open", "storybook:build": "storybook build -o ./dist/storybook -c .storybook", @@ -47,8 +47,9 @@ "not IE 11" ], "dependencies": { - "@emotion/css": "11.11.2", - "@emotion/react": "11.11.4", + "@emotion/css": "11.13.0", + "@emotion/react": "11.13.3", + "@emotion/serialize": "1.3.2", "@floating-ui/react": "0.26.24", "@grafana/data": "11.3.0-pre", "@grafana/e2e-selectors": "11.3.0-pre", @@ -110,12 +111,12 @@ "slate-react": "0.22.10", "tinycolor2": "1.6.0", "tslib": "2.7.0", - "uplot": "1.6.30", + "uplot": "1.6.31", "uuid": "9.0.1" }, "devDependencies": { "@babel/core": "7.25.2", - "@faker-js/faker": "^8.4.1", + "@faker-js/faker": "^9.0.0", "@grafana/tsconfig": "^2.0.0", "@rollup/plugin-node-resolve": "15.3.0", "@storybook/addon-a11y": "^8.1.6", @@ -174,13 +175,13 @@ "react-select-event": "^5.1.0", "react-test-renderer": "18.2.0", "rimraf": "6.0.1", - "rollup": "2.79.1", + "rollup": "^4.22.4", "rollup-plugin-copy": "3.5.0", - "rollup-plugin-dts": "^5.0.0", - "rollup-plugin-esbuild": "5.0.0", - "rollup-plugin-node-externals": "^5.0.0", - "rollup-plugin-svg-import": "1.6.0", - "sass-loader": "14.2.1", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "6.1.1", + "rollup-plugin-node-externals": "^7.1.3", + "rollup-plugin-svg-import": "3.0.0", + "sass-loader": "16.0.2", "storybook": "^8.1.6", "storybook-dark-mode": "^4.0.1", "style-loader": "4.0.0", diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts index 76891bf60c7..b18618dd7ea 100644 --- a/packages/grafana-ui/rollup.config.ts +++ b/packages/grafana-ui/rollup.config.ts @@ -1,37 +1,47 @@ import resolve from '@rollup/plugin-node-resolve'; +import { createRequire } from 'node:module'; import path from 'path'; import copy from 'rollup-plugin-copy'; import dts from 'rollup-plugin-dts'; import esbuild from 'rollup-plugin-esbuild'; -import { externals } from 'rollup-plugin-node-externals'; +import { nodeExternals } from 'rollup-plugin-node-externals'; import svg from 'rollup-plugin-svg-import'; -const icons = require('../../public/app/core/icons/cached.json'); - -const pkg = require('./package.json'); +const rq = createRequire(import.meta.url); +const icons = rq('../../public/app/core/icons/cached.json'); +const pkg = rq('./package.json'); const iconSrcPaths = icons.map((iconSubPath) => { return `../../public/img/icons/${iconSubPath}.svg`; }); +const legacyOutputDefaults = { + esModule: true, + interop: 'compat', +}; + export default [ { input: 'src/index.ts', plugins: [ - externals({ deps: true, packagePath: './package.json' }), + nodeExternals({ deps: true, packagePath: './package.json' }), svg({ stringify: true }), resolve(), copy({ targets: [{ src: iconSrcPaths, dest: './dist/public/' }], flatten: false, }), - esbuild(), + esbuild({ + target: 'es2018', + tsconfig: 'tsconfig.build.json', + }), ], output: [ { format: 'cjs', sourcemap: true, dir: path.dirname(pkg.publishConfig.main), + ...legacyOutputDefaults, }, { format: 'esm', @@ -40,6 +50,7 @@ export default [ preserveModules: true, // @ts-expect-error (TS cannot assure that `process.env.PROJECT_CWD` is a string) preserveModulesRoot: path.join(process.env.PROJECT_CWD, `packages/grafana-ui/src`), + ...legacyOutputDefaults, }, ], }, diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx index d5846b3025a..386e6fe56b9 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.story.tsx @@ -18,7 +18,7 @@ const meta: Meta = { args: { loading: undefined, invalid: undefined, - width: 30, + width: undefined, placeholder: 'Select an option...', options: [ { label: 'Apple', value: 'apple' }, @@ -107,6 +107,14 @@ const ManyOptionsStory: StoryFn = ({ numberOfOptions, ...arg ); }; +export const AutoSize: StoryObj = { + args: { + width: 'auto', + minWidth: 5, + maxWidth: 200, + }, +}; + export const ManyOptions: StoryObj = { args: { numberOfOptions: 1e5, diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.tsx index 33eeed308eb..d38df728201 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.tsx @@ -6,6 +6,7 @@ import { useCallback, useId, useMemo, useState } from 'react'; import { useStyles2 } from '../../themes'; import { t } from '../../utils/i18n'; import { Icon } from '../Icon/Icon'; +import { AutoSizeInput } from '../Input/AutoSizeInput'; import { Input, Props as InputProps } from '../Input/Input'; import { getComboboxStyles } from './getComboboxStyles'; @@ -17,15 +18,33 @@ export type ComboboxOption = { description?: string; }; -interface ComboboxProps - extends Omit { +interface ComboboxBaseProps + extends Omit { isClearable?: boolean; createCustomValue?: boolean; options: Array>; onChange: (option: ComboboxOption | null) => void; value: T | null; + /** + * Defaults to 100%. Number is a multiple of 8px. 'auto' will size the input to the content. + * */ + width?: number | 'auto'; } +type AutoSizeConditionals = + | { + width: 'auto'; + minWidth: number; + maxWidth?: number; + } + | { + width?: number; + minWidth?: never; + maxWidth?: never; + }; + +type ComboboxProps = ComboboxBaseProps & AutoSizeConditionals; + function itemToString(item: ComboboxOption | null) { return item?.label ?? item?.value.toString() ?? ''; } @@ -54,6 +73,7 @@ export const Combobox = ({ isClearable = false, createCustomValue = false, id, + width, 'aria-labelledby': ariaLabelledBy, ...restProps }: ComboboxProps) => { @@ -158,9 +178,12 @@ export const Combobox = ({ setInputValue(selectedItem?.label ?? value?.toString() ?? ''); }, [selectedItem, setInputValue, value]); + const InputComponent = width === 'auto' ? AutoSizeInput : Input; + return (
      - {!!value && value === selectedItem?.value && isClearable && ( diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.test.tsx new file mode 100644 index 00000000000..f7de1db5fed --- /dev/null +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { dateTime } from '@grafana/data'; + +import { TimeRangeInput } from './TimeRangeInput'; + +describe('TimeRangeInput', () => { + // TODO: This test is evergreen - the check that we haven't accidentally closed + // the picker still passes without the appropriate fix + // Seems to be related to jest-dom and how it handles clicking outside the node etc. + it('handles selecting dates over multiple months', async () => { + const user = userEvent.setup(); + const from = dateTime('2024-01-01T00:00:00Z'); + const to = dateTime('2024-01-01T00:00:00Z'); + const onChange = jest.fn(); + + render( + { + const { from, to } = payload; + onChange({ from: from.toString(), to: to.toString() }); + }} + value={{ + from, + to, + raw: { + from, + to, + }, + }} + /> + ); + + // TimeRangeInput renders as a button that looks like an input - + // the only one we can see at the start is the button to open the picker + await user.click(screen.getByRole('button')); + + const [firstOpenCalendarButton] = await screen.findAllByRole('button', { name: /open calendar/i }); + await user.click(firstOpenCalendarButton); + + // Select two dates that are on different "screens" of the calendar picker - this is where the bug would occur + await user.click(await screen.findByLabelText(/january 1, 2024/i)); + await user.click(await screen.findByLabelText(/next month/i)); + await user.click(await screen.findByLabelText(/february 28, 2024/i)); + + await user.click(await screen.findByText(/apply time range/i)); + + expect(onChange).toHaveBeenCalledWith({ + from: 'Mon Jan 01 2024 00:00:00 GMT+0000', + to: 'Wed Feb 28 2024 23:59:59 GMT+0000', + }); + }); +}); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx index 12e910661a2..d07788bf97e 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx @@ -1,11 +1,13 @@ import { css, cx } from '@emotion/css'; -import { FormEvent, MouseEvent, useState } from 'react'; +import { useDialog } from '@react-aria/dialog'; +import { FocusScope } from '@react-aria/focus'; +import { useOverlay } from '@react-aria/overlays'; +import { createRef, FormEvent, MouseEvent, useState } from 'react'; import { dateTime, getDefaultTimeRange, GrafanaTheme2, TimeRange, TimeZone } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../themes/ThemeContext'; -import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; import { Icon } from '../Icon/Icon'; import { getInputStyles } from '../Input/Input'; @@ -77,6 +79,22 @@ export const TimeRangeInput = ({ onChange({ from, to, raw: { from, to } }); }; + const overlayRef = createRef(); + const buttonRef = createRef(); + + const { dialogProps } = useDialog({}, overlayRef); + + const { overlayProps } = useOverlay( + { + onClose, + isDismissable: true, + isOpen, + shouldCloseOnInteractOutside: (element) => { + return !buttonRef.current?.contains(element); + }, + }, + overlayRef + ); return (
      {isOpen && ( - - - + +
      + +
      +
      )}
      ); diff --git a/packages/grafana-ui/src/components/Forms/InlineField.tsx b/packages/grafana-ui/src/components/Forms/InlineField.tsx index 8750e300122..2eb4b5c1f25 100644 --- a/packages/grafana-ui/src/components/Forms/InlineField.tsx +++ b/packages/grafana-ui/src/components/Forms/InlineField.tsx @@ -45,6 +45,7 @@ export const InlineField = ({ error, transparent, interactive, + validationMessageHorizontalOverflow, ...htmlProps }: Props) => { const theme = useTheme2(); @@ -72,7 +73,11 @@ export const InlineField = ({
      {cloneElement(children, { invalid, disabled, loading })} {invalid && error && ( -
      +
      {error}
      )} @@ -100,5 +105,13 @@ const getStyles = (theme: GrafanaTheme2, grow?: boolean, shrink?: boolean) => { fieldValidationWrapper: css({ marginTop: theme.spacing(0.5), }), + validationMessageHorizontalOverflow: css({ + width: 0, + overflowX: 'visible', + + '& > *': { + whiteSpace: 'nowrap', + }, + }), }; }; diff --git a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx index 6e5b4ccd837..de855ff5302 100644 --- a/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx +++ b/packages/grafana-ui/src/components/Input/AutoSizeInput.tsx @@ -32,7 +32,7 @@ export const AutoSizeInput = React.forwardRef((props, r // Update internal state when controlled `value` prop changes useEffect(() => { - if (controlledValue !== undefined) { + if (controlledValue) { setValue(controlledValue); } }, [controlledValue]); diff --git a/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx b/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx index 8c3a5a632a6..e1f1e5258e2 100644 --- a/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx +++ b/packages/grafana-ui/src/components/Layout/utils/responsiveness.tsx @@ -1,4 +1,4 @@ -import { CSSInterpolation } from '@emotion/css'; +import { CSSInterpolation } from '@emotion/serialize'; import { GrafanaTheme2, ThemeBreakpointsKey } from '@grafana/data'; diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx index a2a97b3ef8e..3efd06c618e 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldValueMatcher.tsx @@ -62,7 +62,7 @@ export const FieldValueMatcherEditor = ({ options, onChange }: Props) => { ); const opts = options ?? {}; - const isBool = isBooleanReducer(options.reducer); + const isBool = isBooleanReducer(opts.reducer); return (
      diff --git a/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx b/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx index 789a90a1087..f07c3abff22 100644 --- a/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx +++ b/packages/grafana-ui/src/components/VizLegend/VizLegend.tsx @@ -16,6 +16,8 @@ import { mapMouseEventToMode } from './utils'; */ export function VizLegend({ items, + thresholdItems, + mappingItems, displayMode, sortBy: sortKey, seriesVisibilityChangeBehavior = SeriesVisibilityChangeBehavior.Isolate, @@ -83,6 +85,24 @@ export function VizLegend({ [onToggleSeriesVisibility, onLabelClick, seriesVisibilityChangeBehavior] ); + const makeVizLegendList = useCallback( + (items: VizLegendItem[]) => { + return ( + + className={className} + placement={placement} + onLabelMouseOver={onMouseOver} + onLabelMouseOut={onMouseOut} + onLabelClick={onLegendLabelClick} + itemRenderer={itemRenderer} + readonly={readonly} + items={items} + /> + ); + }, + [className, placement, onMouseOver, onMouseOut, onLegendLabelClick, itemRenderer, readonly] + ); + switch (displayMode) { case LegendDisplayMode.Table: return ( @@ -102,17 +122,19 @@ export function VizLegend({ /> ); case LegendDisplayMode.List: + const isThresholdsEnabled = thresholdItems && thresholdItems.length > 1; + const isValueMappingEnabled = mappingItems && mappingItems.length > 0; return ( - - className={className} - items={items} - placement={placement} - onLabelMouseOver={onMouseOver} - onLabelMouseOut={onMouseOut} - onLabelClick={onLegendLabelClick} - itemRenderer={itemRenderer} - readonly={readonly} - /> + <> + {/* render items when single series and there is no thresholds and no value mappings + * render items when multi series and there is no thresholds + */} + {!isThresholdsEnabled && (!isValueMappingEnabled || items.length > 1) && makeVizLegendList(items)} + {/* render threshold colors if From thresholds scheme selected */} + {isThresholdsEnabled && makeVizLegendList(thresholdItems)} + {/* render value mapping colors */} + {isValueMappingEnabled && makeVizLegendList(mappingItems)} + ); default: return null; diff --git a/packages/grafana-ui/src/components/VizLegend/types.ts b/packages/grafana-ui/src/components/VizLegend/types.ts index cf718a64756..4032276bf6a 100644 --- a/packages/grafana-ui/src/components/VizLegend/types.ts +++ b/packages/grafana-ui/src/components/VizLegend/types.ts @@ -12,6 +12,8 @@ export interface VizLegendBaseProps { placement: LegendPlacement; className?: string; items: Array>; + thresholdItems?: Array>; + mappingItems?: Array>; seriesVisibilityChangeBehavior?: SeriesVisibilityChangeBehavior; onLabelClick?: (item: VizLegendItem, event: React.MouseEvent) => void; itemRenderer?: (item: VizLegendItem, index: number) => JSX.Element; diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index ca4ef351874..99428ce1d56 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data'; -import { Button, ButtonProps, DataLinkButton, Stack } from '..'; +import { Button, Stack, TextLink } from '..'; import { useStyles2 } from '../../themes'; import { ActionButton } from '../Actions/ActionButton'; @@ -15,14 +15,19 @@ interface VizTooltipFooterProps { export const ADD_ANNOTATION_ID = 'add-annotation-button'; const renderDataLinks = (dataLinks: LinkModel[]) => { - const buttonProps: ButtonProps = { - variant: 'secondary', - }; - return ( {dataLinks.map((link, i) => ( - + + {link.title} + ))} ); diff --git a/packages/grafana-ui/src/themes/GlobalStyles/utilityClasses.ts b/packages/grafana-ui/src/themes/GlobalStyles/utilityClasses.ts index c1f392c096c..4d52642a762 100644 --- a/packages/grafana-ui/src/themes/GlobalStyles/utilityClasses.ts +++ b/packages/grafana-ui/src/themes/GlobalStyles/utilityClasses.ts @@ -1,5 +1,5 @@ -import { CSSInterpolation } from '@emotion/css'; import { css } from '@emotion/react'; +import { CSSInterpolation } from '@emotion/serialize'; import { GrafanaTheme2 } from '@grafana/data'; diff --git a/pkg/aggregator/go.mod b/pkg/aggregator/go.mod index 6b0c3c779f7..c57a765062d 100644 --- a/pkg/aggregator/go.mod +++ b/pkg/aggregator/go.mod @@ -4,12 +4,12 @@ go 1.23.1 require ( github.com/emicklei/go-restful/v3 v3.11.0 - github.com/grafana/grafana-plugin-sdk-go v0.250.0 + github.com/grafana/grafana-plugin-sdk-go v0.251.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel v1.30.0 k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 k8s.io/apiserver v0.31.0 @@ -119,15 +119,15 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v3 v3.5.14 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect diff --git a/pkg/aggregator/go.sum b/pkg/aggregator/go.sum index 55fbbd6b5b2..99753ff55f8 100644 --- a/pkg/aggregator/go.sum +++ b/pkg/aggregator/go.sum @@ -130,8 +130,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQWUh6L85c6EJUN8Jdo= -github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= +github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I= github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA= @@ -344,30 +344,30 @@ go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 h1:IVtyPth4Rs5P8wIf0mP2KVKFNTJ4paX9qQ4Hkh5gFdc= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 h1:+YPiqF5rR6PqHBlmEFLPumbSP0gY0WmCGFayXRcCLvs= -go.opentelemetry.io/contrib/propagators/jaeger v1.29.0/go.mod h1:6PD7q7qquWSp3Z4HeM3e/2ipRubaY1rXZO8NIHVDZjs= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 h1:g8+Y+7lnhH1DB0THjPPthzQ+RlzAntmTz8+TH2sRU0k= +go.opentelemetry.io/contrib/propagators/jaeger v1.30.0/go.mod h1:lRMaD/FjOQJ2yz/MwOHYxP/BTCMFodNW/wuYDkJvdA4= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 h1:qKi9ntCcronqWqfuKxqrxZlZd82jXJEgGiAWH1+phxo= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0/go.mod h1:1kbAgQa5lgYC3rC6cE3jSxQ/Q13l33wv/WI8U+htwag= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 5612fd4a639..df6afa33be1 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -587,7 +587,7 @@ func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Res // where is the type of annotation with id . // If annotationPermissionUpdate feature toggle is enabled, dashboard annotation scope will be resolved to the corresponding // dashboard and folder scopes (eg, "dashboards:uid:", "folders:uid:" etc). -func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, folderSvc folder.Service) (string, accesscontrol.ScopeAttributeResolver) { +func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, folderStore folder.Store) (string, accesscontrol.ScopeAttributeResolver) { prefix := accesscontrol.ScopeAnnotationsProvider.GetResourceScope("") return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) { scopeParts := strings.Split(initialScope, ":") @@ -649,7 +649,7 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature // Append dashboard parent scopes if dashboard is in a folder or the general scope if dashboard is not in a folder if dashboard.FolderUID != "" { scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID)) - inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc) + inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderStore) if err != nil { return nil, err } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 12eea783e5d..5fb09aa7baf 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -397,14 +397,15 @@ func TestAPI_Annotations(t *testing.T) { dashService := &dashboards.FakeDashboardService{} dashService.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{UID: dashUID, FolderUID: folderUID, FolderID: 1}, nil) folderService := &foldertest.FakeService{} + fStore := folder.NewFakeStore() folderService.ExpectedFolder = &folder.Folder{UID: folderUID, ID: 1} folderDB := &foldertest.FakeFolderStore{} folderDB.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(&folder.Folder{UID: folderUID, ID: 1}, nil) hs.DashboardService = dashService hs.folderService = folderService hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) - hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, folderService)) - hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderDB, dashService, folderService)) + hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, fStore)) + hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderDB, dashService, fStore)) }) var body io.Reader if tt.body != "" { @@ -504,7 +505,7 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { features := featuremgmt.WithFeatures(tc.featureToggles...) - prefix, resolver := AnnotationTypeScopeResolver(fakeAnnoRepo, features, dashSvc, &foldertest.FakeService{}) + prefix, resolver := AnnotationTypeScopeResolver(fakeAnnoRepo, features, dashSvc, folder.NewFakeStore()) require.Equal(t, "annotations:id:", prefix) resolved, err := resolver.Resolve(context.Background(), 1, tc.given) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 2ad87a053c6..2a561916821 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -42,7 +42,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder/folderimpl" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" - "github.com/grafana/grafana/pkg/services/libraryelements/model" + libraryelementsfake "github.com/grafana/grafana/pkg/services/libraryelements/fake" "github.com/grafana/grafana/pkg/services/librarypanels" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/live" @@ -271,7 +271,7 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) { hs.starService = startest.NewStarServiceFake() hs.LibraryPanelService = &mockLibraryPanelService{} - hs.LibraryElementService = &mockLibraryElementService{} + hs.LibraryElementService = &libraryelementsfake.LibraryElementService{} pubDashService := publicdashboards.NewFakePublicDashboardService(t) pubDashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil).Maybe() @@ -677,7 +677,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { Cfg: setting.NewCfg(), ProvisioningService: fakeProvisioningService, LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: mockDashboardProvisioningService{}, SQLStore: mockSQLStore, AccessControl: accesscontrolmock.New(), @@ -827,26 +827,28 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr } libraryPanelsService := mockLibraryPanelService{} - libraryElementsService := mockLibraryElementService{} + libraryElementsService := libraryelementsfake.LibraryElementService{} cfg := setting.NewCfg() ac := accesscontrolmock.New() folderPermissions := accesscontrolmock.NewMockedPermissionsService() dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), - dashboardStore, folderStore, db.InitTestDB(t), features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - + db := db.InitTestDB(t) + fStore := folderimpl.ProvideStore(db) + folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), + dashboardStore, folderStore, db, features, cfg, folderPermissions, + supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) if dashboardService == nil { dashboardService, err = service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, - ac, folderSvc, nil, + ac, folderSvc, fStore, nil, ) require.NoError(t, err) } dashboardProvisioningService, err := service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, - ac, folderSvc, nil, + ac, folderSvc, fStore, nil, ) require.NoError(t, err) @@ -909,7 +911,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s QuotaService: quotatest.New(false, nil), pluginStore: &pluginstore.FakePluginStore{}, LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: dashboardService, folderService: folderService, Features: featuremgmt.WithFeatures(), @@ -947,7 +949,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, SQLStore: sqlmock, dashboardVersionService: fakeDashboardVersionService, Features: featuremgmt.WithFeatures(), @@ -990,7 +992,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout Live: newTestLive(t, db.InitTestDB(t)), QuotaService: quotatest.New(false, nil), LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &mockLibraryElementService{}, + LibraryElementService: &libraryelementsfake.LibraryElementService{}, DashboardService: mock, SQLStore: sqlStore, Features: featuremgmt.WithFeatures(), @@ -1050,34 +1052,3 @@ func (m *mockLibraryPanelService) ConnectLibraryPanelsForDashboard(c context.Con func (m *mockLibraryPanelService) ImportLibraryPanelsForDashboard(c context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error { return nil } - -type mockLibraryElementService struct{} - -func (l *mockLibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { - return model.LibraryElementDTO{}, nil -} - -// GetElement gets an element from a UID. -func (l *mockLibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { - return model.LibraryElementDTO{}, nil -} - -// GetElementsForDashboard gets all connected elements for a specific dashboard. -func (l *mockLibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) { - return map[string]model.LibraryElementDTO{}, nil -} - -// ConnectElementsToDashboard connects elements to a specific dashboard. -func (l *mockLibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error { - return nil -} - -// DisconnectElementsFromDashboard disconnects elements from a specific dashboard. -func (l *mockLibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error { - return nil -} - -// DeleteLibraryElementsInFolder deletes all elements for a specific folder. -func (l *mockLibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error { - return nil -} diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 5456100cd0f..c8d7949b48f 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -1,7 +1,6 @@ package api import ( - "context" "errors" "net/http" "strconv" @@ -12,12 +11,10 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" - "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/apimachinery/identity" folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/infra/metrics" internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" @@ -31,7 +28,6 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" "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/util" "github.com/grafana/grafana/pkg/util/errhttp" @@ -219,10 +215,6 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response return apierrors.ToFolderErrorResponse(err) } - if err := hs.setDefaultFolderPermissions(c.Req.Context(), cmd.OrgID, cmd.SignedInUser, folder); err != nil { - hs.log.Error("Could not set the default folder permissions", "folder", folder.Title, "user", cmd.SignedInUser, "error", err) - } - // Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call // Required for cases when caller wants to immediately interact with the newly created object hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser) @@ -236,36 +228,6 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response return response.JSON(http.StatusOK, folderDTO) } -func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, folder *folder.Folder) error { - if !hs.Cfg.RBAC.PermissionsOnCreation("folder") { - return nil - } - - var permissions []accesscontrol.SetResourcePermissionCommand - - if user.IsIdentityType(claims.TypeUser) { - userID, err := user.GetInternalID() - if err != nil { - return err - } - - permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ - UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(), - }) - } - - isNested := folder.ParentUID != "" - if !isNested || !hs.Features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { - permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ - {BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()}, - {BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()}, - }...) - } - - _, err := hs.folderPermissionsService.SetPermissions(ctx, orgID, folder.UID, permissions...) - return err -} - // swagger:route POST /folders/{folder_uid}/move folders moveFolder // // Move folder. diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 58b4b7040d1..590a4ea0b66 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -457,26 +457,28 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog folderStore := folderimpl.ProvideDashboardFolderStore(sc.db) ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - cfg := setting.NewCfg() actionSets := resourcepermissions.NewActionSetService(features) acSvc := acimpl.ProvideOSSService( sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(), features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), ) - + fStore := folderimpl.ProvideStore(sc.db) folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( - cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets) + cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, fStore, acSvc, sc.teamSvc, sc.userSvc, actionSets) require.NoError(b, err) + + folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, + folderStore, sc.db, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( - cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets) + cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, fStore, acSvc, sc.teamSvc, sc.userSvc, actionSets) require.NoError(b, err) dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( sc.cfg, dashStore, folderStore, features, folderPermissions, dashboardPermissions, ac, - folderServiceWithFlagOn, nil, + folderServiceWithFlagOn, fStore, nil, ) require.NoError(b, err) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 04fed03bcca..f9908deaadf 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -253,6 +253,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi authInfoService login.AuthInfoService, storageService store.StorageService, notificationService notifications.Service, dashboardService dashboards.DashboardService, dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service, + folderStore folder.Store, dsGuardian guardian.DatasourceGuardianProvider, dashboardsnapshotsService dashboardsnapshots.Service, pluginSettings pluginSettings.Service, avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, @@ -379,7 +380,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi hs.registerRoutes() // Register access control scope resolver for annotations - hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, features, dashboardService, folderService)) + hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, features, dashboardService, folderStore)) if err := hs.declareFixedRoles(); err != nil { return nil, err diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 1fb9aaa3ce3..f59b09045a3 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -1,8 +1,7 @@ package api import ( - "errors" - + "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/services/authn" @@ -21,8 +20,7 @@ func (hs *HTTPServer) OAuthLogin(reqCtx *contextmodel.ReqContext) { if errorParam := reqCtx.Query("error"); errorParam != "" { errorDesc := reqCtx.Query("error_description") hs.log.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc) - - hs.redirectWithError(reqCtx, errors.New("login provider denied login request"), "error", errorParam, "errorDesc", errorDesc) + hs.redirectWithError(reqCtx, errutil.Unauthorized("oauth.login", errutil.WithPublicMessage(hs.Cfg.OAuthLoginErrorMessage)).Errorf("Login provider denied login request")) return } diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index a83a9400cc5..b628d35e85a 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -21,7 +21,6 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" - pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/caching" @@ -172,7 +171,7 @@ func TestCallResource(t *testing.T) { }, })) middlewares := pluginsintegration.CreateMiddlewares(cfg, &oauthtokentest.Service{}, tracing.InitializeTracerForTest(), &caching.OSSCachingService{}, featuremgmt.WithFeatures(), prometheus.DefaultRegisterer, pluginRegistry) - pc, err := pluginClient.NewDecorator(&fakes.FakePluginClient{ + pc, err := backend.HandlerFromMiddlewares(&fakes.FakePluginClient{ CallResourceHandlerFunc: backend.CallResourceHandlerFunc(func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { return errors.New("something went wrong") diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 256c50d5f6b..f10f1a48d2b 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -144,7 +144,7 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons AngularDetected: pluginDef.Angular.Detected, } - if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { + if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { listItem.IAM = pluginDef.IAM } @@ -484,7 +484,7 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons return response.ErrOrFallback(http.StatusInternalServerError, "Failed to install plugin", err) } - if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { + if hs.Cfg.ManagedServiceAccountsEnabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) { // This is a non-blocking function that verifies that the installer has // the permissions that the plugin requests to have on Grafana. // If we want to make this blocking, the check will have to happen before or during the installation. diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 7aedc5f0d5e..12894a1a103 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -88,7 +88,11 @@ func (hs *HTTPServer) RotateUserAuthTokenRedirect(c *contextmodel.ReqContext) re return response.Redirect(hs.GetRedirectURL(c)) } - return response.Redirect(hs.Cfg.AppSubURL + "/") + redirectTo := c.Query("redirectTo") + if err := hs.ValidateRedirectTo(redirectTo); err != nil { + return response.Redirect(hs.Cfg.AppSubURL + "/") + } + return response.Redirect(hs.Cfg.AppSubURL + redirectTo) } // swagger:route POST /user/auth-tokens/rotate @@ -133,7 +137,6 @@ func (hs *HTTPServer) rotateToken(c *contextmodel.ReqContext) error { IP: ip, UserAgent: c.Req.UserAgent(), }) - if err != nil { return err } diff --git a/pkg/apimachinery/go.mod b/pkg/apimachinery/go.mod index 5783e2404e8..22e1be6cc40 100644 --- a/pkg/apimachinery/go.mod +++ b/pkg/apimachinery/go.mod @@ -32,8 +32,8 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/otel v1.29.0 // indirect - go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/otel v1.30.0 // indirect + go.opentelemetry.io/otel/trace v1.30.0 // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/pkg/apimachinery/go.sum b/pkg/apimachinery/go.sum index ef6c84128f3..fbd7b2d0aba 100644 --- a/pkg/apimachinery/go.sum +++ b/pkg/apimachinery/go.sum @@ -68,10 +68,10 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/pkg/apimachinery/utils/verbs.go b/pkg/apimachinery/utils/verbs.go new file mode 100644 index 00000000000..7ab5086332f --- /dev/null +++ b/pkg/apimachinery/utils/verbs.go @@ -0,0 +1,22 @@ +package utils + +// Kubernetes request verbs +// http://kubernetes.io/docs/reference/access-authn-authz/authorization/#request-verb-resource +const ( + // VerbGet is mapped from HTTP GET for individual resource + VerbGet = "get" + // VerbList is mapped from HTTP GET for collections + VerbList = "list" + // VerbWatch is mapped from HTTP GET for watching an individual resource or collection of resources + VerbWatch = "watch" + // VerbCreate is mapped from HTTP POST + VerbCreate = "create" + // VerbUpdate is mapped from HTTP PUT + VerbUpdate = "update" + // VerbPatch is mapped from HTTP PATCH + VerbPatch = "patch" + // VerbDelete is mapped from HTTP DELETE for individual resources + VerbDelete = "delete" + // VerbDelete is mapped from HTTP DELETE for collections + VerbDeleteCollection = "deletecollection" +) diff --git a/pkg/apis/iam/v0alpha1/types_servier_account.go b/pkg/apis/iam/v0alpha1/types_servier_account.go index 8451095c04d..04e75eda7a2 100644 --- a/pkg/apis/iam/v0alpha1/types_servier_account.go +++ b/pkg/apis/iam/v0alpha1/types_servier_account.go @@ -1,6 +1,8 @@ package v0alpha1 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -12,9 +14,15 @@ type ServiceAccount struct { Spec ServiceAccountSpec `json:"spec,omitempty"` } +func (s ServiceAccount) AuthID() string { + return fmt.Sprintf("%d", s.Spec.InternalID) +} + type ServiceAccountSpec struct { Title string `json:"title,omitempty"` Disabled bool `json:"disabled,omitempty"` + // This is currently used for authorization checks but we don't want to expose it + InternalID int64 `json:"-"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/iam/v0alpha1/types_user.go b/pkg/apis/iam/v0alpha1/types_user.go index 713b2041876..2ea79d2ecdb 100644 --- a/pkg/apis/iam/v0alpha1/types_user.go +++ b/pkg/apis/iam/v0alpha1/types_user.go @@ -1,6 +1,10 @@ package v0alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type User struct { @@ -10,12 +14,18 @@ type User struct { Spec UserSpec `json:"spec,omitempty"` } +func (u User) AuthID() string { + return fmt.Sprintf("%d", u.Spec.InternalID) +} + type UserSpec struct { Name string `json:"name,omitempty"` Login string `json:"login,omitempty"` Email string `json:"email,omitempty"` EmailVerified bool `json:"emailVerified,omitempty"` Disabled bool `json:"disabled,omitempty"` + // This is currently used for authorization checks but we don't want to expose it + InternalID int64 `json:"-"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apiserver/endpoints/filters/jaeger_tracing.go b/pkg/apiserver/endpoints/filters/jaeger_tracing.go new file mode 100644 index 00000000000..444190976ed --- /dev/null +++ b/pkg/apiserver/endpoints/filters/jaeger_tracing.go @@ -0,0 +1,17 @@ +package filters + +import ( + "net/http" + + jaegerpropagator "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/otel/propagation" +) + +// WithExtractJaegerTrace tries to extract remote trace/span from incoming request. +func WithExtractJaegerTrace(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + propagator := jaegerpropagator.Jaeger{} + ctx := propagator.Extract(req.Context(), propagation.HeaderCarrier(req.Header)) + handler.ServeHTTP(w, req.WithContext(ctx)) + }) +} diff --git a/pkg/apiserver/go.mod b/pkg/apiserver/go.mod index 4f90c6b5b1a..df18223a271 100644 --- a/pkg/apiserver/go.mod +++ b/pkg/apiserver/go.mod @@ -8,7 +8,9 @@ require ( github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240701135906-559738ce6ae1 github.com/prometheus/client_golang v1.20.3 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/otel/trace v1.29.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 + go.opentelemetry.io/otel v1.30.0 + go.opentelemetry.io/otel/trace v1.30.0 k8s.io/apimachinery v0.31.0 k8s.io/apiserver v0.31.0 k8s.io/component-base v0.31.0 @@ -63,11 +65,10 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v3 v3.5.14 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/pkg/apiserver/go.sum b/pkg/apiserver/go.sum index 993ed571af9..49e6ba493c6 100644 --- a/pkg/apiserver/go.sum +++ b/pkg/apiserver/go.sum @@ -190,20 +190,22 @@ go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 h1:g8+Y+7lnhH1DB0THjPPthzQ+RlzAntmTz8+TH2sRU0k= +go.opentelemetry.io/contrib/propagators/jaeger v1.30.0/go.mod h1:lRMaD/FjOQJ2yz/MwOHYxP/BTCMFodNW/wuYDkJvdA4= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= diff --git a/pkg/build/go.mod b/pkg/build/go.mod index b3b23bfd27b..eb21e9948a2 100644 --- a/pkg/build/go.mod +++ b/pkg/build/go.mod @@ -33,9 +33,9 @@ require ( github.com/urfave/cli v1.22.15 // @grafana/grafana-backend-group github.com/urfave/cli/v2 v2.27.1 // @grafana/grafana-backend-group go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect; @grafana/plugins-platform-backend - go.opentelemetry.io/otel v1.29.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel v1.30.0 // indirect; @grafana/grafana-backend-group go.opentelemetry.io/otel/sdk v1.29.0 // indirect; @grafana/grafana-backend-group - go.opentelemetry.io/otel/trace v1.29.0 // indirect; @grafana/grafana-backend-group + go.opentelemetry.io/otel/trace v1.30.0 // indirect; @grafana/grafana-backend-group golang.org/x/crypto v0.27.0 // indirect; @grafana/grafana-backend-group golang.org/x/mod v0.20.0 // @grafana/grafana-backend-group golang.org/x/net v0.29.0 // indirect; @grafana/oss-big-tent @grafana/partner-datasources @@ -82,8 +82,8 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/sys v0.25.0 // indirect google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect; @grafana/grafana-backend-group diff --git a/pkg/build/go.sum b/pkg/build/go.sum index b81b24f1e33..e348659ac5e 100644 --- a/pkg/build/go.sum +++ b/pkg/build/go.sum @@ -229,10 +229,10 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 h1:oM0GTNKGlc5qHctWeIGTVyda4iFFalOzMZ3Ehj5rwB4= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88/go.mod h1:JGG8ebaMO5nXOPnvKEl+DiA4MGwFjCbjsxT1WHIEBPY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.2.0-alpha h1:z2s6Zba+OUyayRv5m1AXWNUTGh57K1iMhy6emU5QT5Y= @@ -245,14 +245,14 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0J go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc= go.opentelemetry.io/otel/log v0.2.0-alpha h1:ixOPvMzserpqA07SENHvRzkZOsnG0XbPr74hv1AQ+n0= go.opentelemetry.io/otel/log v0.2.0-alpha/go.mod h1:vbFZc65yq4c4ssvXY43y/nIqkNJLxORrqw0L85P59LA= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/sdk/log v0.2.0-alpha h1:jGTkL/jroJ31jnP6jDl34N/mDOfRGGYZHcHsCM+5kWA= go.opentelemetry.io/otel/sdk/log v0.2.0-alpha/go.mod h1:Hd8Lw9FPGUM3pfY7iGMRvFaC2Nyau4Ajb5WnQ9OdIho= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index d8624578220..e09d84a9721 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -51,7 +51,10 @@ func validateInput(c utils.CommandLine) error { fileInfo, err := os.Stat(pluginsDir) if err != nil { - if err = os.MkdirAll(pluginsDir, 0o750); err != nil { + // If the directory does not exist, try to create it with permissions enough + // so the server running Grafana can write to it to install new plugins. + // nolint: gosec + if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil { return fmt.Errorf("pluginsDir (%s) is not a writable directory", pluginsDir) } return nil diff --git a/pkg/cmd/grafana-server/commands/buildinfo.go b/pkg/cmd/grafana-server/commands/buildinfo.go index 1051ca520bc..fe694d5e3b1 100644 --- a/pkg/cmd/grafana-server/commands/buildinfo.go +++ b/pkg/cmd/grafana-server/commands/buildinfo.go @@ -5,10 +5,11 @@ import ( "time" "github.com/grafana/grafana/pkg/extensions" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/setting" ) -func getBuildstamp(opts ServerOptions) int64 { +func getBuildstamp(opts standalone.BuildInfo) int64 { buildstampInt64, err := strconv.ParseInt(opts.BuildStamp, 10, 64) if err != nil || buildstampInt64 == 0 { buildstampInt64 = time.Now().Unix() @@ -16,7 +17,7 @@ func getBuildstamp(opts ServerOptions) int64 { return buildstampInt64 } -func SetBuildInfo(opts ServerOptions) { +func SetBuildInfo(opts standalone.BuildInfo) { setting.BuildVersion = opts.Version setting.BuildCommit = opts.Commit setting.EnterpriseBuildCommit = opts.EnterpriseCommit diff --git a/pkg/cmd/grafana-server/commands/cli.go b/pkg/cmd/grafana-server/commands/cli.go index 6e82a6362b4..5f438aad2fd 100644 --- a/pkg/cmd/grafana-server/commands/cli.go +++ b/pkg/cmd/grafana-server/commands/cli.go @@ -21,38 +21,29 @@ import ( "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/process" "github.com/grafana/grafana/pkg/server" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/setting" ) -type ServerOptions struct { - Version string - Commit string - EnterpriseCommit string - BuildBranch string - BuildStamp string - Context *cli.Context -} - func ServerCommand(version, commit, enterpriseCommit, buildBranch, buildstamp string) *cli.Command { return &cli.Command{ Name: "server", Usage: "run the grafana server", Flags: commonFlags, Action: func(context *cli.Context) error { - return RunServer(ServerOptions{ + return RunServer(standalone.BuildInfo{ Version: version, Commit: commit, EnterpriseCommit: enterpriseCommit, BuildBranch: buildBranch, BuildStamp: buildstamp, - Context: context, - }) + }, context) }, Subcommands: []*cli.Command{TargetCommand(version, commit, buildBranch, buildstamp)}, } } -func RunServer(opts ServerOptions) error { +func RunServer(opts standalone.BuildInfo, cli *cli.Context) error { if Version || VerboseVersion { if opts.EnterpriseCommit != gcli.DefaultCommitValue && opts.EnterpriseCommit != "" { fmt.Printf("Version %s (commit: %s, branch: %s, enterprise-commit: %s)\n", opts.Version, opts.Commit, opts.BuildBranch, opts.EnterpriseCommit) @@ -106,7 +97,7 @@ func RunServer(opts ServerOptions) error { Config: ConfigFile, HomePath: HomePath, // tailing arguments have precedence over the options string - Args: append(configOptions, opts.Context.Args().Slice()...), + Args: append(configOptions, cli.Args().Slice()...), }) if err != nil { return err diff --git a/pkg/cmd/grafana-server/commands/target.go b/pkg/cmd/grafana-server/commands/target.go index 25df8f70e04..39f322396ac 100644 --- a/pkg/cmd/grafana-server/commands/target.go +++ b/pkg/cmd/grafana-server/commands/target.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/server" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" "github.com/grafana/grafana/pkg/setting" ) @@ -22,18 +23,17 @@ func TargetCommand(version, commit, buildBranch, buildstamp string) *cli.Command Usage: "target specific grafana dskit services", Flags: commonFlags, Action: func(context *cli.Context) error { - return RunTargetServer(ServerOptions{ + return RunTargetServer(standalone.BuildInfo{ Version: version, Commit: commit, BuildBranch: buildBranch, BuildStamp: buildstamp, - Context: context, - }) + }, context) }, } } -func RunTargetServer(opts ServerOptions) error { +func RunTargetServer(opts standalone.BuildInfo, cli *cli.Context) error { if Version || VerboseVersion { fmt.Printf("Version %s (commit: %s, branch: %s)\n", opts.Version, opts.Commit, opts.BuildBranch) if VerboseVersion { @@ -83,7 +83,7 @@ func RunTargetServer(opts ServerOptions) error { Config: ConfigFile, HomePath: HomePath, // tailing arguments have precedence over the options string - Args: append(configOptions, opts.Context.Args().Slice()...), + Args: append(configOptions, cli.Args().Slice()...), }) if err != nil { return err diff --git a/pkg/cmd/grafana/apiserver/apiserver.md b/pkg/cmd/grafana/apiserver/apiserver.md deleted file mode 100644 index 41fe0362570..00000000000 --- a/pkg/cmd/grafana/apiserver/apiserver.md +++ /dev/null @@ -1,60 +0,0 @@ -# grafana apiserver (standalone) - -The example-apiserver closely resembles the -[sample-apiserver](https://github.com/kubernetes/sample-apiserver/tree/master) project in code and thus -allows the same -[CLI flags](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/) as kube-apiserver. -It is currently used for testing our deployment pipelines for aggregated servers. You can optionally omit the -aggregation path altogether and just run this example apiserver as a standalone process. - -## Standalone Mode - -### Usage - -For setting `--grafana.authn.signing-keys-url`, Grafana must be run with `idForwarding = true` while also ensuring -you have logged in to the instance at least once. - -```shell -go run ./pkg/cmd/grafana apiserver \ - --runtime-config=example.grafana.app/v0alpha1=true \ - --grafana-apiserver-dev-mode \ - --grafana.authn.signing-keys-url="http://localhost:3000/api/signing-keys/keys" \ - --verbosity 10 \ - --secure-port 7443 -``` - -### Verify that all works - -In dev mode, the standalone server's loopback kubeconfig is written to `./data/grafana-apiserver/apiserver.kubeconfig`. - -```shell -export KUBECONFIG=./data/grafana-apiserver/apiserver.kubeconfig - -kubectl api-resources -NAME SHORTNAMES APIVERSION NAMESPACED KIND -dummy example.grafana.app/v0alpha1 true DummyResource -runtime example.grafana.app/v0alpha1 false RuntimeInfo -``` - -### Observability - -Logs, metrics and traces are supported. See `--grafana.log.*`, `--grafana.metrics.*` and `--grafana.tracing.*` flags for details. - -```shell -go run ./pkg/cmd/grafana apiserver \ - --runtime-config=example.grafana.app/v0alpha1=true \ - --help -``` - -For example, to enable debug logs, metrics and traces (using [self-instrumentation](../../../../devenv/docker/blocks/self-instrumentation/readme.md)) use the following: - -```shell -go run ./pkg/cmd/grafana apiserver \ - --runtime-config=example.grafana.app/v0alpha1=true \ - --secure-port=7443 \ - --grafana.log.level=debug \ - --verbosity=10 \ - --grafana.metrics.enable \ - --grafana.tracing.jaeger.address=http://localhost:14268/api/traces \ - --grafana.tracing.sampler-param=1 -``` diff --git a/pkg/cmd/grafana/apiserver/cmd.go b/pkg/cmd/grafana/apiserver/cmd.go deleted file mode 100644 index a187772550e..00000000000 --- a/pkg/cmd/grafana/apiserver/cmd.go +++ /dev/null @@ -1,157 +0,0 @@ -package apiserver - -import ( - "context" - "os" - "sync" - - "github.com/spf13/cobra" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" - genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/component-base/cli" - - "github.com/grafana/grafana/pkg/cmd/grafana-server/commands" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/server" - "github.com/grafana/grafana/pkg/services/apiserver/standalone" -) - -func newCommandStartStandaloneAPIServer(o *APIServerOptions, stopCh <-chan struct{}) *cobra.Command { - devAcknowledgementNotice := "The apiserver command is in heavy development. The entire setup is subject to change without notice" - runtimeConfig := "" - - factory, err := server.InitializeAPIServerFactory() - if err != nil { - return nil - } - o.factory = factory - - cmd := &cobra.Command{ - Use: "apiserver [api group(s)]", - Short: "Run the grafana apiserver", - Long: "Run a standalone kubernetes based apiserver that can be aggregated by a root apiserver. " + - devAcknowledgementNotice, - Example: "grafana apiserver --runtime-config=example.grafana.app/v0alpha1=true", - RunE: func(c *cobra.Command, args []string) error { - if err := log.SetupConsoleLogger("debug"); err != nil { - return nil - } - - if err := o.Validate(); err != nil { - return err - } - - runtime, err := standalone.ReadRuntimeConfig(runtimeConfig) - if err != nil { - return err - } - apis, err := o.factory.GetEnabled(runtime) - if err != nil { - return err - } - - // Currently TracingOptions.ApplyTo, which will configure/initialize tracing, - // happens after loadAPIGroupBuilders. Hack to workaround this for now to allow - // the tracer to be initialized at a later stage, when tracer is available. - // TODO: Fix so that TracingOptions.ApplyTo happens before or during loadAPIGroupBuilders. - tracer := newLateInitializedTracingService() - - ctx, cancel := context.WithCancel(c.Context()) - go func() { - <-stopCh - cancel() - }() - - // Load each group from the args - if err := o.loadAPIGroupBuilders(ctx, tracer, apis); err != nil { - return err - } - - // Finish the config (a noop for now) - if err := o.Complete(); err != nil { - return err - } - - // o.Config(tracer) definitely needs to happen before we override the tracer below - // using tracer.InitTracer with the real tracer - config, err := o.Config(tracer) - if err != nil { - return err - } - - if o.Options.TracingOptions.TracingService != nil { - tracer.InitTracer(o.Options.TracingOptions.TracingService) - } - - defer o.factory.Shutdown() - - if err := o.RunAPIServer(ctx, config); err != nil { - return err - } - - return nil - }, - } - - cmd.Flags().StringVar(&runtimeConfig, "runtime-config", "", "A set of key=value pairs that enable or disable built-in APIs.") - - o.AddFlags(cmd.Flags()) - - return cmd -} - -func RunCLI(opts commands.ServerOptions) int { - stopCh := genericapiserver.SetupSignalHandler() - - commands.SetBuildInfo(opts) - - options := newAPIServerOptions(os.Stdout, os.Stderr) - cmd := newCommandStartStandaloneAPIServer(options, stopCh) - - return cli.Run(cmd) -} - -type lateInitializedTracingProvider struct { - trace.TracerProvider - tracer *lateInitializedTracingService -} - -func (tp lateInitializedTracingProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer { - return tp.tracer.getTracer() -} - -type lateInitializedTracingService struct { - tracing.Tracer - mutex sync.RWMutex -} - -func newLateInitializedTracingService() *lateInitializedTracingService { - ts := &lateInitializedTracingService{ - Tracer: tracing.NewNoopTracerService(), - } - - tp := &lateInitializedTracingProvider{ - tracer: ts, - } - - otel.SetTracerProvider(tp) - - return ts -} - -func (s *lateInitializedTracingService) getTracer() tracing.Tracer { - s.mutex.RLock() - t := s.Tracer - s.mutex.RUnlock() - return t -} - -func (s *lateInitializedTracingService) InitTracer(tracer *tracing.TracingService) { - s.mutex.Lock() - s.Tracer = tracer - s.mutex.Unlock() -} - -var _ tracing.Tracer = &lateInitializedTracingService{} diff --git a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/apiservice.yaml b/pkg/cmd/grafana/apiserver/deploy/aggregator-test/apiservice.yaml deleted file mode 100644 index 65cc2a5884a..00000000000 --- a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/apiservice.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: apiregistration.k8s.io/v1 -kind: APIService -metadata: - name: v0alpha1.example.grafana.app -spec: - version: v0alpha1 - insecureSkipTLSVerify: true - group: example.grafana.app - groupPriorityMinimum: 1000 - versionPriority: 15 - service: - name: example-apiserver - namespace: grafana - port: 7443 diff --git a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/externalname.yaml b/pkg/cmd/grafana/apiserver/deploy/aggregator-test/externalname.yaml deleted file mode 100644 index 75009779106..00000000000 --- a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/externalname.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: service.grafana.app/v0alpha1 -kind: ExternalName -metadata: - name: example-apiserver - namespace: grafana -spec: - host: localhost - diff --git a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/kustomization.yaml b/pkg/cmd/grafana/apiserver/deploy/aggregator-test/kustomization.yaml deleted file mode 100644 index 15829bf3c27..00000000000 --- a/pkg/cmd/grafana/apiserver/deploy/aggregator-test/kustomization.yaml +++ /dev/null @@ -1,3 +0,0 @@ -resources: - - apiservice.yaml - - externalname.yaml diff --git a/pkg/cmd/grafana/apiserver/server.go b/pkg/cmd/grafana/apiserver/server.go deleted file mode 100644 index 96bdc935f8a..00000000000 --- a/pkg/cmd/grafana/apiserver/server.go +++ /dev/null @@ -1,201 +0,0 @@ -package apiserver - -import ( - "context" - "fmt" - "io" - "net" - "path" - - "github.com/grafana/pyroscope-go/godeltaprof/http/pprof" - "github.com/spf13/pflag" - "k8s.io/apimachinery/pkg/runtime/schema" - utilerrors "k8s.io/apimachinery/pkg/util/errors" - genericapiserver "k8s.io/apiserver/pkg/server" - "k8s.io/apiserver/pkg/server/mux" - "k8s.io/client-go/tools/clientcmd" - netutils "k8s.io/utils/net" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - grafanaAPIServer "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/apiserver/standalone" - standaloneoptions "github.com/grafana/grafana/pkg/services/apiserver/standalone/options" - "github.com/grafana/grafana/pkg/services/apiserver/utils" - "github.com/grafana/grafana/pkg/setting" -) - -const ( - dataPath = "data/grafana-apiserver" // same as grafana core -) - -// APIServerOptions contains the state for the apiserver -type APIServerOptions struct { - factory standalone.APIServerFactory - builders []builder.APIGroupBuilder - Options *standaloneoptions.Options - AlternateDNS []string - logger log.Logger - - StdOut io.Writer - StdErr io.Writer -} - -func newAPIServerOptions(out, errOut io.Writer) *APIServerOptions { - logger := log.New("grafana-apiserver") - - return &APIServerOptions{ - logger: logger, - StdOut: out, - StdErr: errOut, - Options: standaloneoptions.New(logger, grafanaAPIServer.Codecs.LegacyCodec()), - } -} - -func (o *APIServerOptions) loadAPIGroupBuilders(ctx context.Context, tracer tracing.Tracer, apis []schema.GroupVersion) error { - o.builders = []builder.APIGroupBuilder{} - for _, gv := range apis { - api, err := o.factory.MakeAPIServer(ctx, tracer, gv) - if err != nil { - return err - } - o.builders = append(o.builders, api) - } - - if len(o.builders) < 1 { - return fmt.Errorf("no apis matched ") - } - - // Install schemas - for _, b := range o.builders { - if err := b.InstallSchema(grafanaAPIServer.Scheme); err != nil { - return err - } - } - return nil -} - -func (o *APIServerOptions) Config(tracer tracing.Tracer) (*genericapiserver.RecommendedConfig, error) { - if err := o.Options.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts( - "localhost", o.AlternateDNS, []net.IP{netutils.ParseIPSloppy("127.0.0.1")}, - ); err != nil { - return nil, fmt.Errorf("error creating self-signed certificates: %v", err) - } - - o.Options.RecommendedOptions.Authentication.RemoteKubeConfigFileOptional = true - - // TODO: determine authorization, currently insecure because Authorization provided by recommended options doesn't work - // reason: an aggregated server won't be able to post subjectaccessreviews (Grafana doesn't have this kind) - // exact error: the server could not find the requested resource (post subjectaccessreviews.authorization.k8s.io) - o.Options.RecommendedOptions.Authorization = nil - - o.Options.RecommendedOptions.Admission = nil - o.Options.RecommendedOptions.Etcd = nil - - if o.Options.RecommendedOptions.CoreAPI.CoreAPIKubeconfigPath == "" { - o.Options.RecommendedOptions.CoreAPI = nil - } - - serverConfig := genericapiserver.NewRecommendedConfig(grafanaAPIServer.Codecs) - - if err := o.Options.ApplyTo(serverConfig); err != nil { - return nil, fmt.Errorf("failed to apply options to server config: %w", err) - } - - if factoryOptions := o.factory.GetOptions(); factoryOptions != nil { - err := factoryOptions.ApplyTo(serverConfig) - if err != nil { - return nil, fmt.Errorf("factory's applyTo func failed: %s", err.Error()) - } - } - - serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("generic-apiserver-start-informers") - serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("priority-and-fairness-config-consumer") - - // Add OpenAPI specs for each group+version - err := builder.SetupConfig( - grafanaAPIServer.Scheme, - serverConfig, - o.builders, - setting.BuildStamp, - setting.BuildVersion, - setting.BuildCommit, - setting.BuildBranch, - o.factory.GetBuildHandlerChainFunc(tracer, o.builders), - ) - return serverConfig, err -} - -func (o *APIServerOptions) AddFlags(fs *pflag.FlagSet) { - o.Options.AddFlags(fs) - - if factoryOptions := o.factory.GetOptions(); factoryOptions != nil { - factoryOptions.AddFlags(fs) - } -} - -// Validate validates APIServerOptions -func (o *APIServerOptions) Validate() error { - errors := make([]error, 0) - - if factoryOptions := o.factory.GetOptions(); factoryOptions != nil { - errors = append(errors, factoryOptions.ValidateOptions()...) - } - - if errs := o.Options.Validate(); len(errs) > 0 { - errors = append(errors, errs...) - } - - return utilerrors.NewAggregate(errors) -} - -// Complete fills in fields required to have valid data -func (o *APIServerOptions) Complete() error { - return nil -} - -func (o *APIServerOptions) RunAPIServer(ctx context.Context, config *genericapiserver.RecommendedConfig) error { - delegationTarget := genericapiserver.NewEmptyDelegate() - completedConfig := config.Complete() - - server, err := completedConfig.New("standalone-apiserver", delegationTarget) - if err != nil { - return err - } - - // Install the API Group+version - // #TODO figure out how to configure storage type in o.Options.StorageOptions - err = builder.InstallAPIs(grafanaAPIServer.Scheme, grafanaAPIServer.Codecs, server, config.RESTOptionsGetter, o.builders, o.Options.StorageOptions, - o.Options.MetricsOptions.MetricsRegisterer, nil, nil, nil, // no need for server lock in standalone - ) - if err != nil { - return err - } - - // write the local config to disk - if o.Options.ExtraOptions.DevMode { - if err = clientcmd.WriteToFile( - utils.FormatKubeConfig(server.LoopbackClientConfig), - path.Join(dataPath, "apiserver.kubeconfig"), - ); err != nil { - return err - } - } - - if config.EnableProfiling { - deltaProfiling{}.Install(server.Handler.NonGoRestfulMux) - } - - return server.PrepareRun().RunWithContext(ctx) -} - -// deltaProfiling adds godeltapprof handlers for pprof under /debug/pprof. -type deltaProfiling struct{} - -// Install register godeltapprof handlers to the given mux. -func (d deltaProfiling) Install(c *mux.PathRecorderMux) { - c.UnlistedHandleFunc("/debug/pprof/delta_heap", pprof.Heap) - c.UnlistedHandleFunc("/debug/pprof/delta_block", pprof.Block) - c.UnlistedHandleFunc("/debug/pprof/delta_mutex", pprof.Mutex) -} diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/README.md b/pkg/cmd/grafana/apiserver/testdata/certificates/README.md deleted file mode 100644 index e048128c8fb..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# apiserver-certificates - -These certificates are used for development and testing ONLY. They are generated using the script under -[hack/make-aggregator-pki.sh](./hack/make-aggregator-pki.sh). - -The CA, server and client certificates are each 10 years of expiration. diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt b/pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt deleted file mode 100755 index 1991fe2da6d..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/ca.crt +++ /dev/null @@ -1,24 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID9zCCAt+gAwIBAgIUeRrA5l+Rl4LkHPP1DmMFlYzrhW4wDQYJKoZIhvcNAQEL -BQAwgYoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApOZXcgU3dlZGVuMRMwEQYDVQQH -DApTdG9ja2hvbG0gMRAwDgYDVQQKDAdHcmFmYW5hMQwwCgYDVQQLDANSJkQxEDAO -BgNVBAMMB3Rlc3QtY2ExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZ3JhZmFuYS5hcHAw -HhcNMjQwODA1MjMxMzUzWhcNMzQwODAzMjMxMzUzWjCBijELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV -BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G -CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBANj6qsutYwof0e0zHrp35Dey+kQxi+VTr/sAKlSoyySP4fmQ -9Qn8mDY4HyJ1oOJFpFAlD0Qp1xGdbvcrlvjoieqmfenW342fza0wqS5K8qkd2rJ7 -khdAE2mACZTFSjmAa8+1rIRWnR0SaHBmDgdxBfNkET+n+cX+WsDMhmzNvPoPDS/V -8LaNih/eOUzb/5hamvD8CNLKakes0u/EsdxOsGFWCkpE1mg9yg0YPms5qUAj9pdV -iPH8B5zA1JoukZCrVGPv6R76fJI1LEohiASNFt9cgs2dhdk6QHzGyqNq3T3Cw8yI -Cug/Kk9DGqwq9OeXtADa4hhPebj04C4hxk0AT2UCAwEAAaNTMFEwHQYDVR0OBBYE -FBBn2SXiiItJQsJZ7MTvIn1s1t3jMB8GA1UdIwQYMBaAFBBn2SXiiItJQsJZ7MTv -In1s1t3jMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABcCcwsg -jrcNZheAaKXtdcZT01+doQhaOvhZQhY5L+bCnB0wkBXRJzv8ne+KgLx7auFQP8/2 -OoPgaiA0R+XtImCkyBd+cr4mo2tYVpHt9+B0HaYGzGoXt0dre47ihlgkqoSwmgvG -9++pfrbQGRd5Xb/j0468sd5uQy1PPhsjCzFZTuxXcaAN13MDNikYjjn5mc5coklu -hCFH54PgP/PUDXxI0v/QUjNOj7hAdMkqOjzFD9Fze1KjtS3aSZvaaZVrM3x/YS8y -1IUgyocgoOKCqBOeEict+g/xghFDe7r2Dlgps/hPD1ojijBl83g5i079jW4y9jm+ -osFOTGnRx2u0CpE= ------END CERTIFICATE----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/ca.key b/pkg/cmd/grafana/apiserver/testdata/certificates/ca.key deleted file mode 100755 index 2b427227dff..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/ca.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDY+qrLrWMKH9Ht -Mx66d+Q3svpEMYvlU6/7ACpUqMskj+H5kPUJ/Jg2OB8idaDiRaRQJQ9EKdcRnW73 -K5b46Inqpn3p1t+Nn82tMKkuSvKpHdqye5IXQBNpgAmUxUo5gGvPtayEVp0dEmhw -Zg4HcQXzZBE/p/nF/lrAzIZszbz6Dw0v1fC2jYof3jlM2/+YWprw/AjSympHrNLv -xLHcTrBhVgpKRNZoPcoNGD5rOalAI/aXVYjx/AecwNSaLpGQq1Rj7+ke+nySNSxK -IYgEjRbfXILNnYXZOkB8xsqjat09wsPMiAroPypPQxqsKvTnl7QA2uIYT3m49OAu -IcZNAE9lAgMBAAECggEAYUTyNy+J1BqSrd66WkZv6SZTeimp+Mrs+71FvMEUnFXi -LFJ2/xydEcVT88s+reEheYo7j0egcfWdLrH8UqZQWYB8ts0MV715YzgKx8Vyhizr -gxLRWZnwed2brfVJwoBXFHzxkzwO398GMckWZfCdhdBoyRwg5UkS3xZw9qq+mmw8 -hvt88sDzHwnk/9rMY6KLNhWNiCSAxU65AlpktWRGy2e9wAtyzN+WX/iQwsiAyhk2 -TV2bHQxRE7FT9hg4UBxzWruYRj4jkLKdH4tVqVKLsV7KutAOiYj3hCjYzOac/5QG -XrTRhN/ewAqJCTOI9K8BVT27l3QxREcq480auDwIawKBgQDzOr6+ILgPdqdGX9Nu -HoLnNuU6OdFsTJ3qXSp4AGv8ufq47PJmTp3F2ZN9RTDr6Fih7UKhApVvFOy6/Hlr -vb0MbL9FOe+2ejtnVmNi13AMexW+nmnOs1oSghXYIbt51/n+5bFmzAo6RFDEG0YT -mtRWMj4mifMI0XH31yRk+Yjp6wKBgQDkXxdG0OmoDwlrlbHjVOpEg7/A0diBHfb9 -yh4MbaVmd6rGgdNkJYq1qB6ctJcLYpKq1QGgj6FyNDNyY/T4lIxRuSHqC2v1Mjt/ -TaL1BhMsH2Q6+bT8mlmM4cNNgSVHfISJv2mMuKfEqb+uY0y8UU6/lYmwzC5rH4bs -TgPNSLmH7wKBgQCk8F9M2y81/UZt6Kmd8T7fwFAt7etgP4yO02LrQY35Mb0eDkBK -tGE1O9hSiMsmDseb9yLJwNDJJS1rl65XK7G5bT0/mow9+CG0b9axvlqTfBxAyXgC -3YjlKCXcDPPvKlCzU9u7U/5TiOQkOEKLJOF9GlEfHUkb37wjT1e0yarYxQKBgFNF -nT40RU8DlKLHJeNH/lhXVh9gJTsHix2Fiqlrfck8T2gsxMEas1aD5A2uB/mdyu9B -1mMOnIcBI9VNP3E48WWHRSeLXKU+2NUVoRsJSQpos+qRTP5i5c5qMAXd1pMXg1ib -FEi8uGgMoZlcGgn89+MCCwANo8tp5o/Z7qb3Ire/AoGANmc8BAGgKanJVAfu7gDU -AgxRg5T0/C6vpWh5k/gFS7AQqgnx8EcRYtuCD4Er5GFRdcyb9AuZccyKzWI5JS89 -YOmJPrfDd7VzBsrOjwJCXcV6tUUq6yO4Ra8vKlIw7T+mMYZh7xWwGnT8UA+X9F+7 -yV6kPpraMD/dFbo2TZoNMjc= ------END PRIVATE KEY----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/client.crt b/pkg/cmd/grafana/apiserver/testdata/certificates/client.crt deleted file mode 100755 index 4b451c2fac9..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/client.crt +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEjDCCA3SgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBijELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV -BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G -CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDAeFw0yNDA4MDUyMzEzNTRaFw0z -NDA4MDMyMzEzNTRaMC8xFDASBgNVBAMMC2RldmVsb3BtZW50MRcwFQYDVQQKDA5z -eXN0ZW06bWFzdGVyczCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMz3 -SSM62uPCNG7mbdlgE0bYcpT8f1ZFmyaauXzbqdTF0/VxGCh9YVBFnqlAdfAUFasN -HBLQz63PniOWbzmhXZBkrCF/nozg7PgiXJ7aM5ppJyYulOI3sldH49V8KWzcGiJo -ZE13kKIlCqnOanErj4RnaxVJvMmWOlt6Zn/ljkDFvwypaOeIX/YO1zuZ95BqGpDh -u4CXipKeCM0AjXNvKrHCi5QbNEF0FMyvsX+s76wZtZSpB7vbIfj/h5Z4Scfyovmy -cZeqgL2mDAnosKw8/KYAgkcArMHoScTfdCjWthJQlPybJKgMKOJI9OehuP1a89A9 -t+T/T8ZT75x0YADw+56WDcsYGECiaixrkpB0aiPBtgDm1i1z7ooqSfAfZa0PEgPT -L680TK66qXI2GFtNqUkU6+xZEfvuxOdZRXkuIVeQ4UMqaiW9y37ppvKzX4llOD2D -VP86D0JxCCuJqxlzSAGbxK61c8mdqM1SKAa5O5Kl0KHxVkGk4zHYtrHqWsy6YOIM -CqLMA2vLAeqYwSEn2eQBkTNdhSulrB6JPNBDPNB9+wX5EOX3w+x9lRxn59kQgKyV -anf1cm1UYeqjsLPhBjpuwVtHhEAc8uJrSZBM3oKPSYEYxwEo+QG8rcKIpjV9gtCu -T3e/WBWlDVBm0NwbxtY/rYc5ggPHMtPhFxBDhjJpAgMBAAGjVzBVMBMGA1UdJQQM -MAoGCCsGAQUFBwMCMB0GA1UdDgQWBBQ+z7tF4/VANXrXgsY0PVQFFo6csjAfBgNV -HSMEGDAWgBQQZ9kl4oiLSULCWezE7yJ9bNbd4zANBgkqhkiG9w0BAQsFAAOCAQEA -NagU3hqJr5wEU79202Rj+aqbzWJlz1jvZVR6PHILB0deTtvYk1EXeVgyjwmz7PW3 -DLYcpwgW3bTdxhejduFNzKazDOZZ2blUlZlHs1PoBb/ipnw4g+ozO+ZyGs05gCu8 -DrsxUKX7bpVcKGfPNVg8L4xZbanizO1XUiv6PDBBRZhKXSl+KO9+aN2C/yRnYmpV -9dyuMI9nFoMB0K7rxTxiRCIPIWs8nsGouLa6lg6/I+xTAjV0IqNz0rQ66UWJOr3x -vLGFdMMaDUSbsNlu18/sCJd+G0rkh24YE6e3I1wGqE9jr4iYsMhkhfp0u4Qojmfp -3/7IGZYzvVIN/PvSFBBgZw== ------END CERTIFICATE----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/client.csr b/pkg/cmd/grafana/apiserver/testdata/certificates/client.csr deleted file mode 100755 index b6848a532aa..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/client.csr +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIEmjCCAoICAQAwLzEUMBIGA1UEAwwLZGV2ZWxvcG1lbnQxFzAVBgNVBAoMDnN5 -c3RlbTptYXN0ZXJzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzPdJ -Izra48I0buZt2WATRthylPx/VkWbJpq5fNup1MXT9XEYKH1hUEWeqUB18BQVqw0c -EtDPrc+eI5ZvOaFdkGSsIX+ejODs+CJcntozmmknJi6U4jeyV0fj1XwpbNwaImhk -TXeQoiUKqc5qcSuPhGdrFUm8yZY6W3pmf+WOQMW/DKlo54hf9g7XO5n3kGoakOG7 -gJeKkp4IzQCNc28qscKLlBs0QXQUzK+xf6zvrBm1lKkHu9sh+P+HlnhJx/Ki+bJx -l6qAvaYMCeiwrDz8pgCCRwCswehJxN90KNa2ElCU/JskqAwo4kj056G4/Vrz0D23 -5P9PxlPvnHRgAPD7npYNyxgYQKJqLGuSkHRqI8G2AObWLXPuiipJ8B9lrQ8SA9Mv -rzRMrrqpcjYYW02pSRTr7FkR++7E51lFeS4hV5DhQypqJb3Lfumm8rNfiWU4PYNU -/zoPQnEIK4mrGXNIAZvErrVzyZ2ozVIoBrk7kqXQofFWQaTjMdi2sepazLpg4gwK -oswDa8sB6pjBISfZ5AGRM12FK6WsHok80EM80H37BfkQ5ffD7H2VHGfn2RCArJVq -d/VybVRh6qOws+EGOm7BW0eEQBzy4mtJkEzego9JgRjHASj5AbytwoimNX2C0K5P -d79YFaUNUGbQ3BvG1j+thzmCA8cy0+EXEEOGMmkCAwEAAaAmMCQGCSqGSIb3DQEJ -DjEXMBUwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggIBAB7x -82Hy942/7nSbz9gJUsJ70sYcre0VRduIMzP1Ytk/ni+1QXDZSmdSrgR0cbTL4VIZ -zSt5Bp2vxBJqHmGPOXv5oWI+nGrfWzmzBGoGNk4cdNW2ZZCn6Om4L16gLYIlxmm1 -vgCgnPOYMEi5qtgG6676wSX7gf5+j1gWVDEwNFil8wBUIlA9QRbc2NN0T6479Z4b -4tUHzUpGuf788OPUqAdxpkH1xZBKW8HEGsk1+NdHgb5krE2ElwvN9qWX5f2DzzwL -sQf3A2IrO4lrso4RHao4X3V+/DOZCx+FK/oXqzyoS4YNnNy6B0377LgYkuuJsbOo -IFC7vRhBORvAXEXD/scLEsGKuQzs1vLLiwOp7pBOcGZzxOoqoXc2A6h64qpZKriq -T1rK6rmrttaXqYUokjbg/ggpcXCC1BGxsoRvGQoN6aQNV27zUh2wpZqpCpVLoN21 -eCqM9LVoPMCRqn7ItXE9oJhasPlKDO6amHL3CtxjvU2meXoa+nCia8lL4Sb9pSkB -O5eX4k3H/m4zjrpbqp7UdnidcYn4zfrjRBr65bqNr1sOxwUDWOa0kqaOKrgqwhWn -ld6zkuekuwzrOK4+Dpf5ybWnVFi8WSz6k9TqQIaeMrzeCBVboufl/ygoIRsTqp7N -WcKuZdUx8t16hFZ/NgpHRgfZI9pYdzi2vNSZ6WMM ------END CERTIFICATE REQUEST----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/client.key b/pkg/cmd/grafana/apiserver/testdata/certificates/client.key deleted file mode 100755 index 0a195f34480..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/client.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDM90kjOtrjwjRu -5m3ZYBNG2HKU/H9WRZsmmrl826nUxdP1cRgofWFQRZ6pQHXwFBWrDRwS0M+tz54j -lm85oV2QZKwhf56M4Oz4Ilye2jOaaScmLpTiN7JXR+PVfCls3BoiaGRNd5CiJQqp -zmpxK4+EZ2sVSbzJljpbemZ/5Y5Axb8MqWjniF/2Dtc7mfeQahqQ4buAl4qSngjN -AI1zbyqxwouUGzRBdBTMr7F/rO+sGbWUqQe72yH4/4eWeEnH8qL5snGXqoC9pgwJ -6LCsPPymAIJHAKzB6EnE33Qo1rYSUJT8mySoDCjiSPTnobj9WvPQPbfk/0/GU++c -dGAA8Puelg3LGBhAomosa5KQdGojwbYA5tYtc+6KKknwH2WtDxID0y+vNEyuuqly -NhhbTalJFOvsWRH77sTnWUV5LiFXkOFDKmolvct+6abys1+JZTg9g1T/Og9CcQgr -iasZc0gBm8SutXPJnajNUigGuTuSpdCh8VZBpOMx2Lax6lrMumDiDAqizANrywHq -mMEhJ9nkAZEzXYUrpaweiTzQQzzQffsF+RDl98PsfZUcZ+fZEICslWp39XJtVGHq -o7Cz4QY6bsFbR4RAHPLia0mQTN6Cj0mBGMcBKPkBvK3CiKY1fYLQrk93v1gVpQ1Q -ZtDcG8bWP62HOYIDxzLT4RcQQ4YyaQIDAQABAoICAAP5CpPqMauJZv9bzCGy9aix -bGt5ce/pMTrNiV5zel5yDQZ/FXpqb2WRNeQ0CEfZzHo4arStfgsGy6WcW7tMnAie -6kG2IdZQoUiQJPFcy3QuSS3u/Z/dYmxD69VRCZNZ4vqgq/S0RmDIMkWB515acMGT -82zHapH8jWoawcwalgK+JSf7YXnJvZ9tC0xf0tSRI+2ZPIDKHyuHReZ7ALg4iEWy -CUclgwJTm/gZiIman497dOwviGN3zxf5aaZtCN69Pp4I+1/sKYA+N3yag/smAvkm -PcrfHITKf3c3ZBQLEQl4Qk0GUPlzY+L6n57yQOijQnd6Yhs9kiKnBkLRyXnB3K9T -M/ZDp7BHSO+e+qENsYk7POzgeU8udF3mS/h17/7kXIdvi/7l/6Dh2YlEqjH+p3XX -AKLKwx01c++h9aa9RXU+PpwyZoKpYhe16kMceF/bxbr74w5Ukqzny5iEX1t0oCfn -KncheoJQiIcC2O1pZLGRzxSlizvbReP9fGPV9qFmDMvdawK14472hT+mNZw4fziq -dLGr9MZ3hVThwIl1ylA9qCJQ0VbaD7Z6YIx0fO2owro4M6Xv247/KZQ7hM1rUo3F -crqe82qysOrAysgDjvho4iCPKW9d/kUPqYA7DHUOvHBkvsgUoc2TNmV48moYOaox -BoHBtDN49SyQpHaoG+NxAoIBAQD2dHhiFyQlTUeym6jpc3l0tLt1Y6OTwkNi0vQo -7j+aNSGSPkcQkhObM54G1pE+DDy2pkpS8m2teKGmnXMBK+TLS/WjcuJbLvGAxlY7 -mRzJh1bzL+1aEa8ZOCtc3FcVBdGTvwCInEzx2r5ghWEgBvy6SQcVs76ALfteUDvI -HxZG1Xou3Il3O+vaxK8ure2SJJk710NQfSF0qaUR6qpLnL3zsRS9GXQ1V/6Ek8CQ -0tdMME+reTC7wh51s/oCNh3QoJThsv4DQQD69SBX8kfy1+qaLOxbNAefrTQk6Pd2 -xNCu4szAG7QyiF/5jOzV5JJYVTnrWqsa5obzCcIbjZWTEPSRAoIBAQDU53bHHDSA -S0oGRpxf9gzz2xVqbG+slraCs9pNA9fkcQostATP9pWvnrch40yOTMyXi2ldVBW8 -ZO8R7jVVA8mbSIyPv9a5V/AuqpRX1fFgojaFY3mEw52CYpmg6/thCESBb6ILXB4e -2cv+lmkb3fYGABxzA+VUEVEsBXprqeJdcBlVdXs3/ZB/jO13ASdSAZrguc2Gknrv -9wg7cPkWIH19m/7DXvQxkX+ROsLSft9AJp3wFzAh4lEGe5zTWaHJS+hfxVrniQEX -M/0qTMRD5PYmsm/8vv6Wx4FlLCx9kWYtygh6Za1h7RDd6iMosDlpV4NASla7fTKV -eOmAvi1uNGxZAoIBAQDyJi3B8wrIu82eZ+LmvVawnIMzK9sk6tJa3vqW3MARO/Lo -Rdh9J4msDGNQRLIgTNW6gFi2dwvcTZJGqpy8oewC83c+STqubMlMxZMkq6PlPtzn -xEdpH8by+IVij/vf4/+vMxPLJgdT+qDjJSnw1eyq++XCJQEf4A4C9MJINoMkxctv -D7DhPjbWlDmrm6i41szYRwEUrF2ayrQtjmwULsVUEsFVqxTK9NJWYPXrVb3EVhNx -X3nKgUh8TYFvesyAl8awm7WIbO4RpZdJ0ftvV0ZihZEVa2GyOfPp8Bx0zZxcuOqE -NrQukl/6ScTJw/MmZ1apMES+AZLGaOgXOl2kShyBAoIBAGiC0xi8rL0JuGXKRbsJ -gqQ2OJYMculq9l7EwPWrXFBkeRUmrXIU3rfeFpHJDWyRIKGHqwpIW38moQDRSVbZ -TB8xBucNye8jzuBplfZkLGA+YLsr8JwOloRJuJZ5IOYp888CKK6g4pxMV8o6tZAb -bkjVxyFimTGiapFMgyLUuy0Y+SatS/ZZP5SNbohLhazI4ulL7CsSPs0LG/xp4axN -+KwvZmkrdH3cqZ12FerUouzPyBnymAsaGKIxDfPl/Phejcxtick8xM9KEw2vr2yQ -uZCXaUfNzhXgC5HJlHFzbZAuq+jBftIiWHRHGkk/8H7YmbJ2i4rv02PyfdVYBd8i -VskCggEANfeuwEYrzuraOl0r60S2XCJURl78s8ug4hkG24/zW+VkjabDw6YY9OEO -INVrlw5dSU7JMz9bqms7fXi7MW+qy5Sfl0OaTGP80I3NsQ8T09OYg0ToKJf/IXHn -GQiGbHRHVTrcjL4blERkzLYlJAr3FuWLIFYusGKFRr/PAeq0GY5yJowaYz3Oqpit -6bf5kM4CK8HQyfMvs0lO3OQ66gRjTq6L/GDHJa+yp6jd7n7p3ocpUBMpTdncCnNb -qYmscnU8UIdrdTGKtySCiBAUup8AzS/Z8D7EnYPg3EiNhG0OJEPSPJPKQqFBnF8w -nL1C44m2dHWfIuDuGDs008g/12A53A== ------END PRIVATE KEY----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/server.crt b/pkg/cmd/grafana/apiserver/testdata/certificates/server.crt deleted file mode 100755 index 1a5a8714498..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/server.crt +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIE0jCCA7qgAwIBAgIBAjANBgkqhkiG9w0BAQsFADCBijELMAkGA1UEBhMCVVMx -EzARBgNVBAgMCk5ldyBTd2VkZW4xEzARBgNVBAcMClN0b2NraG9sbSAxEDAOBgNV -BAoMB0dyYWZhbmExDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHdGVzdC1jYTEfMB0G -CSqGSIb3DQEJARYQdGVzdEBncmFmYW5hLmFwcDAeFw0yNDA4MDUyMzEzNTVaFw0z -NDA4MDMyMzEzNTVaMCkxEjAQBgNVBAMMCWxvY2FsaG9zdDETMBEGA1UECgwKYWdn -cmVnYXRlZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMbgGuvCKonn -9KgW58wqgxiykVbbmT82HmhgOBw5CSg7lVnKQlRzfvUtwZX7UhHXYxOCu0no7RqD -Y19QiGOx6SQlaXSMxqPL1PpWMQdX6j/I/H1pHMJ8Ynyd956VlhKUOIQBJMVd46Bg -6IEU4izuJ9uqEuHQrO8lb2eaTXAjsOmtIImxBI7xFsxp42wJyo6ptGetyaNCw7UF -e8xYmBVSLyfiN/drBx/lcwMiDgzfzf8BsavC96Fa/HujXkybAIyVid2DxKODPT9+ -7MA1ZY0/Nz3ySKSxIIbfL+1cPihR2GGy1Qwa/GuuJSGAwzy6D/9am5/K3yR927Bk -OgMUCIlpGlSDY1qFd79o/n9BJ9FJmlTMgIZ6RokaXAU2uaiE6v7YPYGkXoIz3ncc -SBprVom2h3RmM3qDbmXio1gGsDL/2SI+Hpnq2zhCzL28K+bZA6ukYpCNyyaYF9O6 -X6qi56zGCw/igTJgaYKMsNMSoX+3eHSFxj5JeF4bWcC4qTqmzvfz0HhvX+jjky/V -LHCh9ddH1umT/Ss/IxaNarXxXHoUmN33FgFQ+tML4eTvosG6IxUdJ/34qBT+dWk4 -zNIhStTUNiYPbcSfv8qKLHhMAuZOLsTATLsFQt7bzGZwV8HAuCiqzjg9p4R/dRjE -OLhQLZfKkYl99Mgli6v/hooEBSORCulVAgMBAAGjgaIwgZ8wPgYDVR0RBDcwNYIo -djBhbHBoYTEuZXhhbXBsZS5ncmFmYW5hLmFwcC5kZWZhdWx0LnN2Y4IJbG9jYWxo -b3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUFvrZ -B0vSR3B7HcNPCZrZ2H7sFw0wHwYDVR0jBBgwFoAUEGfZJeKIi0lCwlnsxO8ifWzW -3eMwDQYJKoZIhvcNAQELBQADggEBAGYZYrY+eFmgwFp/ohIcWnekdOiCr7qjnmRo -LyNU/EbFz0HUV+yTrtoXLjv8S9D9Yc029Dgj2f31Hp7cOGjUfd0t1DgTBChcFgVr -E5ZbhmKEM0tQiBMI6mHvy6hFT+nc9/yftnndHRUyR4xm6E1dMFqpMyMdYKojRmbn -F6znVTcjBr4OiDnfTUkqYO8kc3I0qvA5ou4jXAJ9mu3UEbEjwc6C2/Mrr48a42Df -rHrwqwnM3DC2+SWVocctk1PRZqMFWypJ9U/HbKPbId79YyHNsI0XtxizV2sX5oXD -1NPPEBeEXZ3Nv/gn4d0h57UoBML9165fRRQSe7CkIl8kZigHGCg= ------END CERTIFICATE----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/server.csr b/pkg/cmd/grafana/apiserver/testdata/certificates/server.csr deleted file mode 100755 index 7fc2e09691f..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/server.csr +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIE3jCCAsYCAQAwKTESMBAGA1UEAwwJbG9jYWxob3N0MRMwEQYDVQQKDAphZ2dy -ZWdhdGVkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxuAa68Iqief0 -qBbnzCqDGLKRVtuZPzYeaGA4HDkJKDuVWcpCVHN+9S3BlftSEddjE4K7SejtGoNj -X1CIY7HpJCVpdIzGo8vU+lYxB1fqP8j8fWkcwnxifJ33npWWEpQ4hAEkxV3joGDo -gRTiLO4n26oS4dCs7yVvZ5pNcCOw6a0gibEEjvEWzGnjbAnKjqm0Z63Jo0LDtQV7 -zFiYFVIvJ+I392sHH+VzAyIODN/N/wGxq8L3oVr8e6NeTJsAjJWJ3YPEo4M9P37s -wDVljT83PfJIpLEght8v7Vw+KFHYYbLVDBr8a64lIYDDPLoP/1qbn8rfJH3bsGQ6 -AxQIiWkaVINjWoV3v2j+f0En0UmaVMyAhnpGiRpcBTa5qITq/tg9gaRegjPedxxI -GmtWibaHdGYzeoNuZeKjWAawMv/ZIj4emerbOELMvbwr5tkDq6RikI3LJpgX07pf -qqLnrMYLD+KBMmBpgoyw0xKhf7d4dIXGPkl4XhtZwLipOqbO9/PQeG9f6OOTL9Us -cKH110fW6ZP9Kz8jFo1qtfFcehSY3fcWAVD60wvh5O+iwbojFR0n/fioFP51aTjM -0iFK1NQ2Jg9txJ+/yooseEwC5k4uxMBMuwVC3tvMZnBXwcC4KKrOOD2nhH91GMQ4 -uFAtl8qRiX30yCWLq/+GigQFI5EK6VUCAwEAAaBwMG4GCSqGSIb3DQEJDjFhMF8w -PgYDVR0RBDcwNYIodjBhbHBoYTEuZXhhbXBsZS5ncmFmYW5hLmFwcC5kZWZhdWx0 -LnN2Y4IJbG9jYWxob3N0MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAN -BgkqhkiG9w0BAQsFAAOCAgEAGmr6CnGfcm+DapmhQnQLyq0HyCqKD0PKKamofQLb -YVMiCF/zKDV4+gIs2kMK3uWeZ9r4xDa9nEgBw6U2vi14AI79hmpETCrfvK6hterI -Znb2TQMYnUY3rt26DFNf3/21/jb/1cn/9z55TaHJGqqlJmvB1LfYJoMN8t6A6xg4 -J8TYjBuwQQOqFAPRuxmGag44PSC9V5e6gajz56RPyZz2kmdbfPZRNCnqileDWZla -7pilwP8QAhrJCPP25edc/5hP2WNTEH/GTa5FFmkNMKEHn6+dnBuMu5w1SygKUYWz -37qE7jntZC/RGVZ//npwsVyaa+NbgJNjhg/EMj+sWb/Eet2ETq7v9FCM0QG3HNUk -6d6af2YHI3Fo89y5ty1DOydBa5lIxy6gDTwameJYoTem71nPlRtU2b4VFBWZ4xwE -ac7Xmon+Z7tOHVwcCPp1cTwJ2TNwha0JxsW0C1g3QG59ILU5FMPlqOuDJi41uWql -Q56O+a6MnK7GfGxBMMf4FSlbV3xjUGxqyGy5KwIcsy+u/3axCWPUcoz3g0pApbGO -pfsu5Ptr/xMtYjLRXbEcH9Byqx/LrBvD2upwNnlfMtgWIlg/EnZ26Mgardu/NwQq -3fpYv00VWbvBE/B/5p5zmEpA67COFejiwVTsbeN345Ue2mq59DNg6BZmaxKH3Sbq -Kz8= ------END CERTIFICATE REQUEST----- diff --git a/pkg/cmd/grafana/apiserver/testdata/certificates/server.key b/pkg/cmd/grafana/apiserver/testdata/certificates/server.key deleted file mode 100755 index 820abf97e95..00000000000 --- a/pkg/cmd/grafana/apiserver/testdata/certificates/server.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDG4BrrwiqJ5/So -FufMKoMYspFW25k/Nh5oYDgcOQkoO5VZykJUc371LcGV+1IR12MTgrtJ6O0ag2Nf -UIhjsekkJWl0jMajy9T6VjEHV+o/yPx9aRzCfGJ8nfeelZYSlDiEASTFXeOgYOiB -FOIs7ifbqhLh0KzvJW9nmk1wI7DprSCJsQSO8RbMaeNsCcqOqbRnrcmjQsO1BXvM -WJgVUi8n4jf3awcf5XMDIg4M383/AbGrwvehWvx7o15MmwCMlYndg8Sjgz0/fuzA -NWWNPzc98kiksSCG3y/tXD4oUdhhstUMGvxrriUhgMM8ug//Wpufyt8kfduwZDoD -FAiJaRpUg2NahXe/aP5/QSfRSZpUzICGekaJGlwFNrmohOr+2D2BpF6CM953HEga -a1aJtod0ZjN6g25l4qNYBrAy/9kiPh6Z6ts4Qsy9vCvm2QOrpGKQjcsmmBfTul+q -ouesxgsP4oEyYGmCjLDTEqF/t3h0hcY+SXheG1nAuKk6ps7389B4b1/o45Mv1Sxw -ofXXR9bpk/0rPyMWjWq18Vx6FJjd9xYBUPrTC+Hk76LBuiMVHSf9+KgU/nVpOMzS -IUrU1DYmD23En7/Kiix4TALmTi7EwEy7BULe28xmcFfBwLgoqs44PaeEf3UYxDi4 -UC2XypGJffTIJYur/4aKBAUjkQrpVQIDAQABAoICAFTLxj7Czc96OvuWtKP9dmNH -9Cd0P63PnfyEFjiWaxyf9yjPUCPhEP9qUJHqFE6uJzzw73lumvZEklDYLidP+ufi -GcpLogDCDt/kc0g9yJAE2v+AG3ajgXzAAA46mr/2Ofiy4iJTS5Sc7VXoeR2OOCl1 -pVJqXuoi7JLgnGcVmL+yBV8gPqDSFBX5ijINJLRakKTqWUDG3VpoaaYyGjpxDdE0 -KAfTNzj25Oivkw0TOiqiZsalPV+rw17WRAVmy7+lnSB5qBTOBwX1UO4NdmzYyO2d -SjMKoSNQs4dB3vDjIN9bWHKuaPVizcsws05HyT1oPVXPMwDEtzDJM2EPoCoyyboB -sIaMnobL3QpfpumLOcpWl+W9rcOCY4fcBU6kgQXRT8XlvSa9hu0UIo4XD9tbYVzq -+REpJlDrocvaWIRjrq6UtcHZzvUZ3YB8v6FZx6lFjZN8dBt8QOoRKgnnAFUTZHT0 -CddFOLMnEeCof+oKF7GrWVAF/BpXHR1hI5Wq7/3wf5MKUOIDj9Z2DM4WzTQCMGEk -PpxCO37xFpH4mOSkZN2lydo1ZBQG0WAc4sFBaJX/dKijci26q9JWbn1fkmQ5XQsG -nXI58QoXFwf7Qlk0zIo0219a9wFR0hls4+JNlyqAoZ6iZevIPR7T954WokaNpvm8 -mZjmrKa59GrSSgyC0IY7AoIBAQDqvTNlKrEaxXaQq97ws3EtkjvpOCkPykRy2Fx6 -nskEtln7AsgJdLu6Q4W0/SYpIDko/cGAKjZLthq8t81As2CdZMiPuJVNufZtziQj -Lai2jcObeuWGMkFMGnODwP3bIoPvcPnsiSt0P/bJc/MPZIZRvgyPzLlF9OvNof2H -Rca8iX42nxZb0V3Uet6n2sqpEsV6uA3R1V1hc0T/b04c3D3GZgOpMXnqfCrIsqdr -6VpeNJZ7O/x8uyHl9kqztqmvlle8bikOZ0s0n93PdcXuHBoWossPZWW08A68B68d -lPldxYeSIkDfL3jDdvN6U12oUARo1hdt0Ozvww7NtS+SPWv3AoIBAQDY41kyOErf -SPVxuMWIhirstDI14xKFbC9DpIO9BALni25toWdsYeAJb4YW+ELUmBKHEGz3kB1N -0mDkqSipXaEE3rbsx44oegmSiOQrGLNypQdEctttE3HJxLCyabh/kbeFVv+L3+8G -VB+0jq/SgtSqZZAwkwwaUwD4/+lVVZ1xjEAliuZ4cNHxtl/DPq8gBR+8nUr4LZac -HEdF6zyMEEAG19Cyzz+zE86bY5IvUQvDjGEnZh71LIG5jns+Lh4b1Nw9JPNmCZ9d -mddIetFd031vI/NkkBuQ0SUqd+eQwDt0jgt2nQEO60Lw9D9UUks/c++M5GkQeGjy -1KyzKOLqTcoTAoIBADBOtXf5XC8lOew14pBobT8ym++35gNg3ctAqW92o+m7WTMl -9GK1yjhf0vFXM3Y9MmY0KpEknr3gAQqbTLsm7xgU+I1TMC6puYQJazhuGg1PiVTC -6t8+EmAGBYW0vslNBhfNiTFbXTz0OOZmXTvqtRW3ZcBmIi66Y5iS4Kjo/Cgqp3W4 -MZK9uHCUxKOIjDJVMZy6qeVn4mq+nRFwJ4Qa8v+UWOaFzxApc2iQE5JKmJVQfzNn -OeO1Yxl/IQpw6eS/rNiTVxGmwjxXNf+OviftUpUb9Wv6sv6UdIPPlQMieFsK3oZ9 -VBpaG6EmJp8i7uBHb1Df1jx8RXZmDvLYeay/xSsCggEBAIqnZTl2xV7TfJ30Gsw5 -wa1LUaIjhY6oZ9rdjJ7Etrqh57nMapreQ2Sk2FtM4SSaB5YzCQaHKkS7Dth/0A/e -XHcJjnX26Um1IvN78iofA3FyUSAQMXkc6iysQq38akebt3BV+s7IHT21gANlCMAS -hbRdc32qNB2MHN4SdG/qaNnTaJrXnpk2vvDAv53JMBnPTMe+4tOgCV3JskLfrPh5 -1wTI6ZG2bqmkKvwp/qWjMVsVHnMalQX2KwSeMunAf90ZCqdIPRZpZmlnVTrv0XMj -Jlhr6kjK2+SL4C+zMeXXDutnd6qfmrKX8laqPuZAKfzpuCYhS42M/MLo9XMf21kg -2+MCggEAepV2WsaeUfXu5MxFdCrsJ+MntBlQDVELULVt8Lw1lJBvs89SQVSF2y4Z -ng8gbZyRZeZe56NXQBgOdRQxxYxfWC8+lfRB0Vx3htteEuuQmMSVsyoLfTcpWHx+ -4aX9WIrnFnjJvPZtVrApu00esjoLOBvTgcn8LTcpK0JvUGTyNKZ2dCYHx0uy07S2 -FIN/zWyrt1+bTXdQnRzgRIfE6pIhPRyxZJk3es+2yY/hgdkf6vdxKYCB+RLCU9jw -CrbmG/OXisvGK3A26Bknje4Rm6/5tdyU8cmqeV4rzaH5QTRWvG/d3qJ9PSWTlX6a -C+DUGz2rplZQ/4PtNOcz7reeAFvu6A== ------END PRIVATE KEY----- diff --git a/pkg/cmd/grafana/main.go b/pkg/cmd/grafana/main.go index e9003dd078a..5679d272a06 100644 --- a/pkg/cmd/grafana/main.go +++ b/pkg/cmd/grafana/main.go @@ -8,8 +8,9 @@ import ( "github.com/urfave/cli/v2" gcli "github.com/grafana/grafana/pkg/cmd/grafana-cli/commands" - gsrv "github.com/grafana/grafana/pkg/cmd/grafana-server/commands" - "github.com/grafana/grafana/pkg/cmd/grafana/apiserver" + "github.com/grafana/grafana/pkg/cmd/grafana-server/commands" + "github.com/grafana/grafana/pkg/server" + "github.com/grafana/grafana/pkg/services/apiserver/standalone" ) // The following variables cannot be constants, since they can be overridden through the -X link flag @@ -31,7 +32,7 @@ func main() { } func MainApp() *cli.App { - return &cli.App{ + app := &cli.App{ Name: "grafana", Usage: "Grafana server and command line interface", Authors: []*cli.Author{ @@ -43,30 +44,32 @@ func MainApp() *cli.App { Version: version, Commands: []*cli.Command{ gcli.CLICommand(version), - gsrv.ServerCommand(version, commit, enterpriseCommit, buildBranch, buildstamp), - { - // The kubernetes standalone apiserver service runner - Name: "apiserver", - Usage: "run a standalone api service (experimental)", - // Skip parsing flags because the command line is actually managed by cobra - SkipFlagParsing: true, - Action: func(context *cli.Context) error { - // exit here because apiserver handles its own error output - os.Exit(apiserver.RunCLI(gsrv.ServerOptions{ - Version: version, - Commit: commit, - EnterpriseCommit: enterpriseCommit, - BuildBranch: buildBranch, - BuildStamp: buildstamp, - Context: context, - })) - return nil - }, - }, + commands.ServerCommand(version, commit, enterpriseCommit, buildBranch, buildstamp), }, CommandNotFound: cmdNotFound, EnableBashCompletion: true, } + + // Set the global build info + buildInfo := standalone.BuildInfo{ + Version: version, + Commit: commit, + EnterpriseCommit: enterpriseCommit, + BuildBranch: buildBranch, + BuildStamp: buildstamp, + } + commands.SetBuildInfo(buildInfo) + + // Add the enterprise command line to build an api server + f, err := server.InitializeAPIServerFactory() + if err == nil { + cmd := f.GetCLICommand(buildInfo) + if cmd != nil { + app.Commands = append(app.Commands, cmd) + } + } + + return app } func cmdNotFound(c *cli.Context, command string) { diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index fc46292a84d..98e324f8e5e 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -15,11 +15,6 @@ import ( _ "github.com/go-jose/go-jose/v3" _ "github.com/gobwas/glob" _ "github.com/googleapis/gax-go/v2" - _ "github.com/grafana/dskit/backoff" - _ "github.com/grafana/dskit/flagext" - _ "github.com/grafana/e2e" - _ "github.com/grafana/gofpdf" - _ "github.com/grafana/gomemcache/memcache" _ "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" _ "github.com/grpc-ecosystem/go-grpc-middleware/v2" _ "github.com/hashicorp/go-multierror" @@ -29,9 +24,16 @@ import ( _ "github.com/phpdave11/gofpdi" _ "github.com/robfig/cron/v3" _ "github.com/russellhaering/goxmldsig" + _ "github.com/spf13/cobra" // used by the standalone apiserver cli _ "github.com/stretchr/testify/require" _ "golang.org/x/time/rate" _ "xorm.io/builder" + + _ "github.com/grafana/dskit/backoff" + _ "github.com/grafana/dskit/flagext" + _ "github.com/grafana/e2e" + _ "github.com/grafana/gofpdf" + _ "github.com/grafana/gomemcache/memcache" ) var IsEnterprise bool = false diff --git a/pkg/infra/usagestats/service/service.go b/pkg/infra/usagestats/service/service.go index ccd9357fb56..8974a3286ae 100644 --- a/pkg/infra/usagestats/service/service.go +++ b/pkg/infra/usagestats/service/service.go @@ -3,6 +3,7 @@ package service import ( "context" "encoding/json" + "sync/atomic" "time" "github.com/grafana/grafana/pkg/api/routing" @@ -27,7 +28,7 @@ type UsageStats struct { externalMetrics []usagestats.MetricsFunc sendReportCallbacks []usagestats.SendReportCallbackFunc - readyToReport bool + readyToReport atomic.Bool } func ProvideService(cfg *setting.Cfg, @@ -79,7 +80,7 @@ func (uss *UsageStats) Run(ctx context.Context) error { for { select { case <-sendReportTicker.C: - if !uss.readyToReport { + if !uss.readyToReport.Load() { nextSendInterval = time.Minute sendReportTicker.Reset(nextSendInterval) continue @@ -114,7 +115,7 @@ func (uss *UsageStats) RegisterSendReportCallback(c usagestats.SendReportCallbac func (uss *UsageStats) SetReadyToReport(context.Context) { uss.log.Info("Usage stats are ready to report") - uss.readyToReport = true + uss.readyToReport.Store(true) } func (uss *UsageStats) supportBundleCollector() supportbundles.Collector { diff --git a/pkg/plugins/ifaces.go b/pkg/plugins/ifaces.go index 5606d4b6259..14684b3bc55 100644 --- a/pkg/plugins/ifaces.go +++ b/pkg/plugins/ifaces.go @@ -88,13 +88,7 @@ type FoundPlugin struct { // Client is used to communicate with backend plugin implementations. type Client interface { - backend.QueryDataHandler - backend.CheckHealthHandler - backend.StreamHandler - backend.AdmissionHandler - backend.ConversionHandler - backend.CallResourceHandler - backend.CollectMetricsHandler + backend.Handler } // BackendFactoryProvider provides a backend factory for a provided plugin. @@ -131,23 +125,6 @@ type Licensing interface { AppURL() string } -// ClientMiddleware is an interface representing the ability to create a middleware -// that implements the Client interface. -type ClientMiddleware interface { - // CreateClientMiddleware creates a new client middleware. - CreateClientMiddleware(next Client) Client -} - -// The ClientMiddlewareFunc type is an adapter to allow the use of ordinary -// functions as ClientMiddleware's. If f is a function with the appropriate -// signature, ClientMiddlewareFunc(f) is a ClientMiddleware that calls f. -type ClientMiddlewareFunc func(next Client) Client - -// CreateClientMiddleware implements the ClientMiddleware interface. -func (fn ClientMiddlewareFunc) CreateClientMiddleware(next Client) Client { - return fn(next) -} - type SignatureCalculator interface { Calculate(ctx context.Context, src PluginSource, plugin FoundPlugin) (Signature, error) } diff --git a/pkg/plugins/manager/client/client.go b/pkg/plugins/manager/client/client.go index 33753947c88..c6382451a87 100644 --- a/pkg/plugins/manager/client/client.go +++ b/pkg/plugins/manager/client/client.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/util/proxyutil" ) const ( @@ -101,8 +102,11 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq removeConnectionHeaders(res.Headers) removeHopByHopHeaders(res.Headers) removeNonAllowedHeaders(res.Headers) + } else { + res.Headers = map[string][]string{} } + proxyutil.SetProxyResponseHeaders(res.Headers) ensureContentTypeHeader(res) } diff --git a/pkg/plugins/manager/client/client_test.go b/pkg/plugins/manager/client/client_test.go index 20401d34250..420754059d2 100644 --- a/pkg/plugins/manager/client/client_test.go +++ b/pkg/plugins/manager/client/client_test.go @@ -310,6 +310,48 @@ func TestCallResource(t *testing.T) { require.Equal(t, "should not be deleted", res.Headers["X-Custom"][0]) }) + t.Run("Should set proxy response headers", func(t *testing.T) { + resHeaders := map[string][]string{ + "X-Custom": {"should not be deleted"}, + } + + req := &backend.CallResourceRequest{ + PluginContext: backend.PluginContext{ + PluginID: "pid", + }, + } + + responses := []*backend.CallResourceResponse{} + sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { + responses = append(responses, res) + return nil + }) + + p.RegisterClient(&fakePluginBackend{ + crr: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return sender.Send(&backend.CallResourceResponse{ + Headers: resHeaders, + Status: http.StatusOK, + Body: []byte(backendResponse), + }) + }, + }) + err := registry.Add(context.Background(), p) + require.NoError(t, err) + + client := ProvideService(registry) + + err = client.CallResource(context.Background(), req, sender) + require.NoError(t, err) + + require.Len(t, responses, 1) + res := responses[0] + require.Equal(t, http.StatusOK, res.Status) + require.Equal(t, []byte(backendResponse), res.Body) + require.Equal(t, "sandbox", res.Headers["Content-Security-Policy"][0]) + require.Equal(t, "should not be deleted", res.Headers["X-Custom"][0]) + }) + t.Run("Should ensure content type header", func(t *testing.T) { tcs := []struct { contentType string diff --git a/pkg/plugins/manager/client/clienttest/clienttest.go b/pkg/plugins/manager/client/clienttest/clienttest.go deleted file mode 100644 index 45886258a20..00000000000 --- a/pkg/plugins/manager/client/clienttest/clienttest.go +++ /dev/null @@ -1,331 +0,0 @@ -package clienttest - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager/client" - "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/web" -) - -type TestClient struct { - plugins.Client - QueryDataFunc backend.QueryDataHandlerFunc - CallResourceFunc backend.CallResourceHandlerFunc - CheckHealthFunc backend.CheckHealthHandlerFunc - CollectMetricsFunc backend.CollectMetricsHandlerFunc - SubscribeStreamFunc func(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) - PublishStreamFunc func(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) - RunStreamFunc func(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error - ValidateAdmissionFunc backend.ValidateAdmissionFunc - MutateAdmissionFunc backend.MutateAdmissionFunc - ConvertObjectsFunc backend.ConvertObjectsFunc -} - -func (c *TestClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if c.QueryDataFunc != nil { - return c.QueryDataFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if c.CallResourceFunc != nil { - return c.CallResourceFunc(ctx, req, sender) - } - - return nil -} - -func (c *TestClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if c.CheckHealthFunc != nil { - return c.CheckHealthFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - if c.CollectMetricsFunc != nil { - return c.CollectMetricsFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - if c.PublishStreamFunc != nil { - return c.PublishStreamFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - if c.SubscribeStreamFunc != nil { - return c.SubscribeStreamFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - if c.RunStreamFunc != nil { - return c.RunStreamFunc(ctx, req, sender) - } - return nil -} - -func (c *TestClient) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - if c.ValidateAdmissionFunc != nil { - return c.ValidateAdmissionFunc(ctx, req) - } - return nil, nil -} - -func (c *TestClient) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - if c.MutateAdmissionFunc != nil { - return c.MutateAdmissionFunc(ctx, req) - } - return nil, nil -} - -func (c *TestClient) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - if c.ConvertObjectsFunc != nil { - return c.ConvertObjectsFunc(ctx, req) - } - return nil, nil -} - -type MiddlewareScenarioContext struct { - QueryDataCallChain []string - CallResourceCallChain []string - CollectMetricsCallChain []string - CheckHealthCallChain []string - SubscribeStreamCallChain []string - PublishStreamCallChain []string - RunStreamCallChain []string - InstanceSettingsCallChain []string - ValidateAdmissionCallChain []string - MutateAdmissionCallChain []string - ConvertObjectsCallChain []string -} - -func (ctx *MiddlewareScenarioContext) NewMiddleware(name string) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - return &TestMiddleware{ - next: next, - Name: name, - sCtx: ctx, - } - }) -} - -type TestMiddleware struct { - next plugins.Client - sCtx *MiddlewareScenarioContext - Name string -} - -func (m *TestMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - m.sCtx.QueryDataCallChain = append(m.sCtx.QueryDataCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.QueryData(ctx, req) - m.sCtx.QueryDataCallChain = append(m.sCtx.QueryDataCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - m.sCtx.CallResourceCallChain = append(m.sCtx.CallResourceCallChain, fmt.Sprintf("before %s", m.Name)) - err := m.next.CallResource(ctx, req, sender) - m.sCtx.CallResourceCallChain = append(m.sCtx.CallResourceCallChain, fmt.Sprintf("after %s", m.Name)) - return err -} - -func (m *TestMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - m.sCtx.CollectMetricsCallChain = append(m.sCtx.CollectMetricsCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.CollectMetrics(ctx, req) - m.sCtx.CollectMetricsCallChain = append(m.sCtx.CollectMetricsCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.ValidateAdmission(ctx, req) - m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.MutateAdmission(ctx, req) - m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - m.sCtx.ConvertObjectsCallChain = append(m.sCtx.ConvertObjectsCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.ConvertObjects(ctx, req) - m.sCtx.ConvertObjectsCallChain = append(m.sCtx.ConvertObjectsCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.CheckHealth(ctx, req) - m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - m.sCtx.SubscribeStreamCallChain = append(m.sCtx.SubscribeStreamCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.SubscribeStream(ctx, req) - m.sCtx.SubscribeStreamCallChain = append(m.sCtx.SubscribeStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - m.sCtx.PublishStreamCallChain = append(m.sCtx.PublishStreamCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.PublishStream(ctx, req) - m.sCtx.PublishStreamCallChain = append(m.sCtx.PublishStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - m.sCtx.RunStreamCallChain = append(m.sCtx.RunStreamCallChain, fmt.Sprintf("before %s", m.Name)) - err := m.next.RunStream(ctx, req, sender) - m.sCtx.RunStreamCallChain = append(m.sCtx.RunStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return err -} - -var _ plugins.Client = &TestClient{} - -type ClientDecoratorTest struct { - T *testing.T - Context context.Context - TestClient *TestClient - Middlewares []plugins.ClientMiddleware - Decorator *client.Decorator - ReqContext *contextmodel.ReqContext - QueryDataReq *backend.QueryDataRequest - QueryDataCtx context.Context - CallResourceReq *backend.CallResourceRequest - CallResourceCtx context.Context - CheckHealthReq *backend.CheckHealthRequest - CheckHealthCtx context.Context - CollectMetricsReq *backend.CollectMetricsRequest - CollectMetricsCtx context.Context - SubscribeStreamReq *backend.SubscribeStreamRequest - SubscribeStreamCtx context.Context - PublishStreamReq *backend.PublishStreamRequest - PublishStreamCtx context.Context - - // When CallResource is called, the sender will be called with these values - callResourceResponses []*backend.CallResourceResponse -} - -type ClientDecoratorTestOption func(*ClientDecoratorTest) - -func NewClientDecoratorTest(t *testing.T, opts ...ClientDecoratorTestOption) *ClientDecoratorTest { - cdt := &ClientDecoratorTest{ - T: t, - Context: context.Background(), - } - cdt.TestClient = &TestClient{ - QueryDataFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - cdt.QueryDataReq = req - cdt.QueryDataCtx = ctx - return nil, nil - }, - CallResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - cdt.CallResourceReq = req - cdt.CallResourceCtx = ctx - if cdt.callResourceResponses != nil { - for _, r := range cdt.callResourceResponses { - if err := sender.Send(r); err != nil { - return err - } - } - } - return nil - }, - CheckHealthFunc: func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - cdt.CheckHealthReq = req - cdt.CheckHealthCtx = ctx - return nil, nil - }, - CollectMetricsFunc: func(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - cdt.CollectMetricsReq = req - cdt.CollectMetricsCtx = ctx - return nil, nil - }, - SubscribeStreamFunc: func(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - cdt.SubscribeStreamReq = req - cdt.SubscribeStreamCtx = ctx - return nil, nil - }, - PublishStreamFunc: func(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - cdt.PublishStreamReq = req - cdt.PublishStreamCtx = ctx - return nil, nil - }, - } - require.NotNil(t, cdt) - - for _, opt := range opts { - opt(cdt) - } - - d, err := client.NewDecorator(cdt.TestClient, cdt.Middlewares...) - require.NoError(t, err) - require.NotNil(t, d) - - cdt.Decorator = d - - return cdt -} - -func WithReqContext(req *http.Request, user *user.SignedInUser) ClientDecoratorTestOption { - return ClientDecoratorTestOption(func(cdt *ClientDecoratorTest) { - if cdt.ReqContext == nil { - cdt.ReqContext = &contextmodel.ReqContext{ - Context: &web.Context{ - Resp: web.NewResponseWriter(req.Method, httptest.NewRecorder()), - }, - SignedInUser: user, - } - } - - cdt.Context = ctxkey.Set(cdt.Context, cdt.ReqContext) - - *req = *req.WithContext(cdt.Context) - cdt.ReqContext.Req = req - }) -} - -func WithMiddlewares(middlewares ...plugins.ClientMiddleware) ClientDecoratorTestOption { - return ClientDecoratorTestOption(func(cdt *ClientDecoratorTest) { - if cdt.Middlewares == nil { - cdt.Middlewares = []plugins.ClientMiddleware{} - } - - cdt.Middlewares = append(cdt.Middlewares, middlewares...) - }) -} - -// WithResourceResponses can be used to make the test client send simulated resource responses back over the sender stream -func WithResourceResponses(responses []*backend.CallResourceResponse) ClientDecoratorTestOption { - return ClientDecoratorTestOption(func(cdt *ClientDecoratorTest) { - cdt.callResourceResponses = responses - }) -} diff --git a/pkg/plugins/manager/client/decorator.go b/pkg/plugins/manager/client/decorator.go deleted file mode 100644 index 90a679d5ebe..00000000000 --- a/pkg/plugins/manager/client/decorator.go +++ /dev/null @@ -1,196 +0,0 @@ -package client - -import ( - "context" - "errors" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/plugins" -) - -// Decorator allows a plugins.Client to be decorated with middlewares. -type Decorator struct { - client plugins.Client - middlewares []plugins.ClientMiddleware -} - -var ( - _ = plugins.Client(&Decorator{}) -) - -// NewDecorator creates a new plugins.client decorator. -func NewDecorator(client plugins.Client, middlewares ...plugins.ClientMiddleware) (*Decorator, error) { - if client == nil { - return nil, errors.New("client cannot be nil") - } - - return &Decorator{ - client: client, - middlewares: middlewares, - }, nil -} - -func (d *Decorator) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if req == nil { - return nil, errNilRequest - } - ctx = backend.WithEndpoint(ctx, backend.EndpointQueryData) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - - return client.QueryData(ctx, req) -} - -func (d *Decorator) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req == nil { - return errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointCallResource) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - if sender == nil { - return errors.New("sender cannot be nil") - } - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.CallResource(ctx, req, sender) -} - -func (d *Decorator) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointCollectMetrics) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.CollectMetrics(ctx, req) -} - -func (d *Decorator) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointCheckHealth) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.CheckHealth(ctx, req) -} - -func (d *Decorator) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointSubscribeStream) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.SubscribeStream(ctx, req) -} - -func (d *Decorator) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointPublishStream) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.PublishStream(ctx, req) -} - -func (d *Decorator) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - if req == nil { - return errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointRunStream) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - if sender == nil { - return errors.New("sender cannot be nil") - } - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.RunStream(ctx, req, sender) -} - -func (d *Decorator) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointValidateAdmission) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.ValidateAdmission(ctx, req) -} - -func (d *Decorator) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointMutateAdmission) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.MutateAdmission(ctx, req) -} - -func (d *Decorator) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - if req == nil { - return nil, errNilRequest - } - - ctx = backend.WithEndpoint(ctx, backend.EndpointConvertObject) - ctx = backend.WithPluginContext(ctx, req.PluginContext) - ctx = backend.WithUser(ctx, req.PluginContext.User) - - client := clientFromMiddlewares(d.middlewares, d.client) - return client.ConvertObjects(ctx, req) -} - -func clientFromMiddlewares(middlewares []plugins.ClientMiddleware, finalClient plugins.Client) plugins.Client { - if len(middlewares) == 0 { - return finalClient - } - - reversed := reverseMiddlewares(middlewares) - next := finalClient - - for _, m := range reversed { - next = m.CreateClientMiddleware(next) - } - - return next -} - -func reverseMiddlewares(middlewares []plugins.ClientMiddleware) []plugins.ClientMiddleware { - reversed := make([]plugins.ClientMiddleware, len(middlewares)) - copy(reversed, middlewares) - - for i, j := 0, len(reversed)-1; i < j; i, j = i+1, j-1 { - reversed[i], reversed[j] = reversed[j], reversed[i] - } - - return reversed -} diff --git a/pkg/plugins/manager/client/decorator_test.go b/pkg/plugins/manager/client/decorator_test.go deleted file mode 100644 index 5ed063233e7..00000000000 --- a/pkg/plugins/manager/client/decorator_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package client - -import ( - "context" - "fmt" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/plugins" -) - -func TestDecorator(t *testing.T) { - var queryDataCalled bool - var callResourceCalled bool - var checkHealthCalled bool - c := &TestClient{ - QueryDataFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - queryDataCalled = true - return nil, nil - }, - CallResourceFunc: func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - callResourceCalled = true - return nil - }, - CheckHealthFunc: func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - checkHealthCalled = true - return nil, nil - }, - } - require.NotNil(t, c) - - ctx := MiddlewareScenarioContext{} - - mwOne := ctx.NewMiddleware("mw1") - mwTwo := ctx.NewMiddleware("mw2") - - d, err := NewDecorator(c, mwOne, mwTwo) - require.NoError(t, err) - require.NotNil(t, d) - - _, _ = d.QueryData(context.Background(), &backend.QueryDataRequest{}) - require.True(t, queryDataCalled) - - sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { - return nil - }) - - _ = d.CallResource(context.Background(), &backend.CallResourceRequest{}, sender) - require.True(t, callResourceCalled) - - _, _ = d.CheckHealth(context.Background(), &backend.CheckHealthRequest{}) - require.True(t, checkHealthCalled) - - require.Len(t, ctx.QueryDataCallChain, 4) - require.EqualValues(t, []string{"before mw1", "before mw2", "after mw2", "after mw1"}, ctx.QueryDataCallChain) - require.Len(t, ctx.CallResourceCallChain, 4) - require.EqualValues(t, []string{"before mw1", "before mw2", "after mw2", "after mw1"}, ctx.CallResourceCallChain) - require.Len(t, ctx.CheckHealthCallChain, 4) - require.EqualValues(t, []string{"before mw1", "before mw2", "after mw2", "after mw1"}, ctx.CheckHealthCallChain) -} - -func TestReverseMiddlewares(t *testing.T) { - t.Run("Should reverse 1 middleware", func(t *testing.T) { - ctx := MiddlewareScenarioContext{} - middlewares := []plugins.ClientMiddleware{ - ctx.NewMiddleware("mw1"), - } - reversed := reverseMiddlewares(middlewares) - require.Len(t, reversed, 1) - require.Equal(t, "mw1", reversed[0].CreateClientMiddleware(nil).(*TestMiddleware).Name) - }) - - t.Run("Should reverse 2 middlewares", func(t *testing.T) { - ctx := MiddlewareScenarioContext{} - middlewares := []plugins.ClientMiddleware{ - ctx.NewMiddleware("mw1"), - ctx.NewMiddleware("mw2"), - } - reversed := reverseMiddlewares(middlewares) - require.Len(t, reversed, 2) - require.Equal(t, "mw2", reversed[0].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw1", reversed[1].CreateClientMiddleware(nil).(*TestMiddleware).Name) - }) - - t.Run("Should reverse 3 middlewares", func(t *testing.T) { - ctx := MiddlewareScenarioContext{} - middlewares := []plugins.ClientMiddleware{ - ctx.NewMiddleware("mw1"), - ctx.NewMiddleware("mw2"), - ctx.NewMiddleware("mw3"), - } - reversed := reverseMiddlewares(middlewares) - require.Len(t, reversed, 3) - require.Equal(t, "mw3", reversed[0].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw2", reversed[1].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw1", reversed[2].CreateClientMiddleware(nil).(*TestMiddleware).Name) - }) - - t.Run("Should reverse 4 middlewares", func(t *testing.T) { - ctx := MiddlewareScenarioContext{} - middlewares := []plugins.ClientMiddleware{ - ctx.NewMiddleware("mw1"), - ctx.NewMiddleware("mw2"), - ctx.NewMiddleware("mw3"), - ctx.NewMiddleware("mw4"), - } - reversed := reverseMiddlewares(middlewares) - require.Len(t, reversed, 4) - require.Equal(t, "mw4", reversed[0].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw3", reversed[1].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw2", reversed[2].CreateClientMiddleware(nil).(*TestMiddleware).Name) - require.Equal(t, "mw1", reversed[3].CreateClientMiddleware(nil).(*TestMiddleware).Name) - }) -} - -type TestClient struct { - plugins.Client - QueryDataFunc backend.QueryDataHandlerFunc - CallResourceFunc backend.CallResourceHandlerFunc - CheckHealthFunc backend.CheckHealthHandlerFunc -} - -func (c *TestClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - if c.QueryDataFunc != nil { - return c.QueryDataFunc(ctx, req) - } - - return nil, nil -} - -func (c *TestClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if c.CallResourceFunc != nil { - return c.CallResourceFunc(ctx, req, sender) - } - - return nil -} - -func (c *TestClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - if c.CheckHealthFunc != nil { - return c.CheckHealthFunc(ctx, req) - } - - return nil, nil -} - -type MiddlewareScenarioContext struct { - QueryDataCallChain []string - CallResourceCallChain []string - CollectMetricsCallChain []string - CheckHealthCallChain []string - SubscribeStreamCallChain []string - PublishStreamCallChain []string - RunStreamCallChain []string - InstanceSettingsCallChain []string - ValidateAdmissionCallChain []string - MutateAdmissionCallChain []string - ConvertObjectCallChain []string -} - -func (ctx *MiddlewareScenarioContext) NewMiddleware(name string) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - return &TestMiddleware{ - next: next, - Name: name, - sCtx: ctx, - } - }) -} - -type TestMiddleware struct { - next plugins.Client - sCtx *MiddlewareScenarioContext - Name string -} - -func (m *TestMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - m.sCtx.QueryDataCallChain = append(m.sCtx.QueryDataCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.QueryData(ctx, req) - m.sCtx.QueryDataCallChain = append(m.sCtx.QueryDataCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - m.sCtx.CallResourceCallChain = append(m.sCtx.CallResourceCallChain, fmt.Sprintf("before %s", m.Name)) - err := m.next.CallResource(ctx, req, sender) - m.sCtx.CallResourceCallChain = append(m.sCtx.CallResourceCallChain, fmt.Sprintf("after %s", m.Name)) - return err -} - -func (m *TestMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - m.sCtx.CollectMetricsCallChain = append(m.sCtx.CollectMetricsCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.CollectMetrics(ctx, req) - m.sCtx.CollectMetricsCallChain = append(m.sCtx.CollectMetricsCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.CheckHealth(ctx, req) - m.sCtx.CheckHealthCallChain = append(m.sCtx.CheckHealthCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - m.sCtx.SubscribeStreamCallChain = append(m.sCtx.SubscribeStreamCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.SubscribeStream(ctx, req) - m.sCtx.SubscribeStreamCallChain = append(m.sCtx.SubscribeStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - m.sCtx.PublishStreamCallChain = append(m.sCtx.PublishStreamCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.PublishStream(ctx, req) - m.sCtx.PublishStreamCallChain = append(m.sCtx.PublishStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - m.sCtx.RunStreamCallChain = append(m.sCtx.RunStreamCallChain, fmt.Sprintf("before %s", m.Name)) - err := m.next.RunStream(ctx, req, sender) - m.sCtx.RunStreamCallChain = append(m.sCtx.RunStreamCallChain, fmt.Sprintf("after %s", m.Name)) - return err -} - -func (m *TestMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.ValidateAdmission(ctx, req) - m.sCtx.ValidateAdmissionCallChain = append(m.sCtx.ValidateAdmissionCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.MutateAdmission(ctx, req) - m.sCtx.MutateAdmissionCallChain = append(m.sCtx.MutateAdmissionCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -func (m *TestMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - m.sCtx.ConvertObjectCallChain = append(m.sCtx.ConvertObjectCallChain, fmt.Sprintf("before %s", m.Name)) - res, err := m.next.ConvertObjects(ctx, req) - m.sCtx.ConvertObjectCallChain = append(m.sCtx.ConvertObjectCallChain, fmt.Sprintf("after %s", m.Name)) - return res, err -} - -var _ plugins.Client = &TestClient{} diff --git a/pkg/promlib/go.mod b/pkg/promlib/go.mod index ba5b99a39c3..aaf7c74f960 100644 --- a/pkg/promlib/go.mod +++ b/pkg/promlib/go.mod @@ -4,15 +4,15 @@ go 1.23.1 require ( github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 - github.com/grafana/grafana-plugin-sdk-go v0.250.0 + github.com/grafana/grafana-plugin-sdk-go v0.251.0 github.com/json-iterator/go v1.1.12 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.20.3 github.com/prometheus/common v0.55.0 github.com/prometheus/prometheus v1.8.2-0.20221021121301-51a44e6657c3 github.com/stretchr/testify v1.9.0 - go.opentelemetry.io/otel v1.29.0 - go.opentelemetry.io/otel/trace v1.29.0 + go.opentelemetry.io/otel v1.30.0 + go.opentelemetry.io/otel/trace v1.30.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa k8s.io/apimachinery v0.31.0 ) @@ -102,12 +102,12 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 // indirect - go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 // indirect go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/atomic v1.11.0 // indirect diff --git a/pkg/promlib/go.sum b/pkg/promlib/go.sum index ae0bdbf758b..62dd38a48aa 100644 --- a/pkg/promlib/go.sum +++ b/pkg/promlib/go.sum @@ -98,8 +98,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 h1:as4PmrFoYI1byS5JjsgPC7uSGTMh+SgS0ePv6hOyDGU= github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3/go.mod h1:lcjGB6SuaZ2o44A9nD6p/tR4QXSPbzViRY520Gy6pTQ= -github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQWUh6L85c6EJUN8Jdo= -github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= +github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w= +github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= @@ -264,30 +264,30 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0 h1:IVtyPth4Rs5P8wIf0mP2KVKFNTJ4paX9qQ4Hkh5gFdc= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.53.0/go.mod h1:ImRBLMJv177/pwiLZ7tU7HDGNdBv7rS0HQ99eN/zBl8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/contrib/propagators/jaeger v1.29.0 h1:+YPiqF5rR6PqHBlmEFLPumbSP0gY0WmCGFayXRcCLvs= -go.opentelemetry.io/contrib/propagators/jaeger v1.29.0/go.mod h1:6PD7q7qquWSp3Z4HeM3e/2ipRubaY1rXZO8NIHVDZjs= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0 h1:sqmsIQ75l6lfZjjpnXXT9DFVtYEDg6CH0/Cn4/3A1Wg= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.55.0/go.mod h1:rsg1EO8LXSs2po50PB5CeY/MSVlhghuKBgXlKnqm6ks= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI= +go.opentelemetry.io/contrib/propagators/jaeger v1.30.0 h1:g8+Y+7lnhH1DB0THjPPthzQ+RlzAntmTz8+TH2sRU0k= +go.opentelemetry.io/contrib/propagators/jaeger v1.30.0/go.mod h1:lRMaD/FjOQJ2yz/MwOHYxP/BTCMFodNW/wuYDkJvdA4= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0 h1:qKi9ntCcronqWqfuKxqrxZlZd82jXJEgGiAWH1+phxo= go.opentelemetry.io/contrib/samplers/jaegerremote v0.23.0/go.mod h1:1kbAgQa5lgYC3rC6cE3jSxQ/Q13l33wv/WI8U+htwag= go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= -go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= -go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= diff --git a/pkg/promlib/library.go b/pkg/promlib/library.go index bd7d48b2fc1..1f35360be54 100644 --- a/pkg/promlib/library.go +++ b/pkg/promlib/library.go @@ -109,7 +109,8 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq return err } - if strings.EqualFold(req.Path, "version-detect") { + switch { + case strings.EqualFold(req.Path, "version-detect"): versionObj, found := i.versionCache.Get("version") if found { return sender.Send(versionObj.(*backend.CallResourceResponse)) @@ -121,6 +122,13 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq } i.versionCache.Set("version", vResp, cache.DefaultExpiration) return sender.Send(vResp) + + case strings.EqualFold(req.Path, "suggestions"): + resp, err := i.resource.GetSuggestions(ctx, req) + if err != nil { + return err + } + return sender.Send(resp) } resp, err := i.resource.Execute(ctx, req) diff --git a/pkg/promlib/library_test.go b/pkg/promlib/library_test.go index 9bdf5d504bd..b5d5d6235bb 100644 --- a/pkg/promlib/library_test.go +++ b/pkg/promlib/library_test.go @@ -117,6 +117,19 @@ func TestService(t *testing.T) { err := service.CallResource(context.Background(), req, sender) require.NoError(t, err) }) + + t.Run("suggest resource", func(t *testing.T) { + f := &fakeHTTPClientProvider{} + httpProvider := getMockPromTestSDKProvider(f) + l := backend.NewLoggerWith("logger", "test") + service := NewService(httpProvider, l, mockExtendTransportOptions) + + req := mockSuggestResource() + sender := &fakeSender{} + err := service.CallResource(context.Background(), req, sender) + require.NoError(t, err) + require.Equal(t, `http://localhost:9090/api/v1/labels?end=2022-06-01T12%3A00%3A00Z&limit=10&match%5B%5D=go_cgo_go_to_c_calls_calls_total%7Bjob%3D~%22.%2B%22%7D&match%5B%5D=up%7Bjob%3D~%22.%2B%22%7D&start=2022-06-01T00%3A00%3A00Z`, f.Roundtripper.Req.URL.String()) + }) } func mockRequest() *backend.CallResourceRequest { @@ -146,3 +159,42 @@ func mockRequest() *backend.CallResourceRequest { Body: []byte("match%5B%5D: ALERTS\nstart: 1655271408\nend: 1655293008"), } } + +func mockSuggestResource() *backend.CallResourceRequest { + return &backend.CallResourceRequest{ + PluginContext: backend.PluginContext{ + OrgID: 0, + PluginID: "prometheus", + User: nil, + AppInstanceSettings: nil, + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ + ID: 0, + UID: "", + Type: "prometheus", + Name: "test-prom", + URL: "http://localhost:9090", + User: "", + Database: "", + BasicAuthEnabled: true, + BasicAuthUser: "admin", + Updated: time.Time{}, + JSONData: []byte("{}"), + }, + }, + Path: "suggestions", + URL: "suggestions", + Method: http.MethodPost, + Body: []byte(` + { + "queries": ["up + 1", "go_cgo_go_to_c_calls_calls_total + 2"], + "scopes": [{ + "key": "job", + "value": ".+", + "operator": "regex-match" + }], + "start": "2022-06-01T00:00:00Z", + "end": "2022-06-01T12:00:00Z", + "limit": 10 + }`), + } +} diff --git a/pkg/promlib/models/query.go b/pkg/promlib/models/query.go index 002102db219..c54e51fea41 100644 --- a/pkg/promlib/models/query.go +++ b/pkg/promlib/models/query.go @@ -207,7 +207,7 @@ func Parse(span trace.Span, query backend.DataQuery, dsScrapeInterval string, in // Interpolate variables in expr timeRange := query.TimeRange.To.Sub(query.TimeRange.From) - expr := interpolateVariables( + expr := InterpolateVariables( model.Expr, query.Interval, calculatedStep, @@ -365,14 +365,14 @@ func calculateRateInterval( return rateInterval } -// interpolateVariables interpolates built-in variables +// InterpolateVariables interpolates built-in variables // expr PromQL query // queryInterval Requested interval in milliseconds. This value may be overridden by MinStep in query options // calculatedStep Calculated final step value. It was calculated in calculatePrometheusInterval // requestedMinStep Requested minimum step value. QueryModel.interval // dsScrapeInterval Data source scrape interval in the config // timeRange Requested time range for query -func interpolateVariables( +func InterpolateVariables( expr string, queryInterval time.Duration, calculatedStep time.Duration, diff --git a/pkg/promlib/models/scope.go b/pkg/promlib/models/scope.go index ec6c8f95b28..3fd7a62b8eb 100644 --- a/pkg/promlib/models/scope.go +++ b/pkg/promlib/models/scope.go @@ -15,7 +15,7 @@ func ApplyFiltersAndGroupBy(rawExpr string, scopeFilters, adHocFilters []ScopeFi return "", err } - matchers, err := filtersToMatchers(scopeFilters, adHocFilters) + matchers, err := FiltersToMatchers(scopeFilters, adHocFilters) if err != nil { return "", err } @@ -70,7 +70,7 @@ func ApplyFiltersAndGroupBy(rawExpr string, scopeFilters, adHocFilters []ScopeFi return expr.String(), nil } -func filtersToMatchers(scopeFilters, adhocFilters []ScopeFilter) ([]*labels.Matcher, error) { +func FiltersToMatchers(scopeFilters, adhocFilters []ScopeFilter) ([]*labels.Matcher, error) { filterMap := make(map[string]*labels.Matcher) for _, filter := range append(scopeFilters, adhocFilters...) { diff --git a/pkg/promlib/resource/resource.go b/pkg/promlib/resource/resource.go index 888267d4138..8635c618dc7 100644 --- a/pkg/promlib/resource/resource.go +++ b/pkg/promlib/resource/resource.go @@ -3,14 +3,20 @@ package resource import ( "bytes" "context" + "encoding/json" "fmt" "net/http" + "net/url" + "slices" + "time" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/data/utils/maputil" + "github.com/prometheus/prometheus/promql/parser" "github.com/grafana/grafana/pkg/promlib/client" + "github.com/grafana/grafana/pkg/promlib/models" "github.com/grafana/grafana/pkg/promlib/utils" ) @@ -84,3 +90,121 @@ func (r *Resource) DetectVersion(ctx context.Context, req *backend.CallResourceR return r.Execute(ctx, newReq) } + +func getSelectors(expr string) ([]string, error) { + parsed, err := parser.ParseExpr(expr) + if err != nil { + return nil, err + } + + selectors := make([]string, 0) + + parser.Inspect(parsed, func(node parser.Node, nodes []parser.Node) error { + switch v := node.(type) { + case *parser.VectorSelector: + for _, matcher := range v.LabelMatchers { + if matcher == nil { + continue + } + if matcher.Name == "__name__" { + selectors = append(selectors, matcher.Value) + } + } + } + return nil + }) + + return selectors, nil +} + +// SuggestionRequest is the request body for the GetSuggestions resource. +type SuggestionRequest struct { + // LabelName, if provided, will result in label values being returned for the given label name. + LabelName string `json:"labelName"` + + Queries []string `json:"queries"` + + Scopes []models.ScopeFilter `json:"scopes"` + AdhocFilters []models.ScopeFilter `json:"adhocFilters"` + + // Start and End are proxied directly to the prometheus endpoint (which is rfc3339 | unix_timestamp) + Start string `json:"start"` + End string `json:"end"` + + // Limit is the maximum number of suggestions to return and is proxied directly to the prometheus endpoint. + Limit int64 `json:"limit"` +} + +// GetSuggestions takes a Suggestion Request in the body of the resource request. +// It builds a to call prometheus' labels endpoint (or label values endpoint if labelName is provided) +// The match parameters for the endpoints are built from metrics extracted from the queries +// combined with the scopes and adhoc filters provided in the request. +// Queries must be valid raw promql. +func (r *Resource) GetSuggestions(ctx context.Context, req *backend.CallResourceRequest) (*backend.CallResourceResponse, error) { + sugReq := SuggestionRequest{} + err := json.Unmarshal(req.Body, &sugReq) + if err != nil { + return nil, fmt.Errorf("error unmarshalling suggestion request: %v", err) + } + + selectorList := []string{} + for _, query := range sugReq.Queries { + // Since we are only extracting selectors from the metric name, we can use dummy + // time durations. + interpolatedQuery := models.InterpolateVariables( + query, + time.Minute, + time.Minute, + "1m", + "15s", + time.Minute, + ) + s, err := getSelectors(interpolatedQuery) + if err != nil { + r.log.Warn("error parsing selectors", "error", err, "query", interpolatedQuery) + continue + } + selectorList = append(selectorList, s...) + } + + slices.Sort(selectorList) + selectorList = slices.Compact(selectorList) + + matchers, err := models.FiltersToMatchers(sugReq.Scopes, sugReq.AdhocFilters) + if err != nil { + return nil, fmt.Errorf("error converting filters to matchers: %v", err) + } + + values := url.Values{} + + for _, s := range selectorList { + vs := parser.VectorSelector{Name: s, LabelMatchers: matchers} + values.Add("match[]", vs.String()) + } + + if sugReq.Start != "" { + values.Add("start", sugReq.Start) + } + if sugReq.End != "" { + values.Add("end", sugReq.End) + } + if sugReq.Limit > 0 { + values.Add("limit", fmt.Sprintf("%d", sugReq.Limit)) + } + + newReq := &backend.CallResourceRequest{ + PluginContext: req.PluginContext, + } + + if sugReq.LabelName != "" { + // Get label values for the given name (key) + newReq.Path = "/api/v1/label/" + sugReq.LabelName + "/values" + newReq.URL = "/api/v1/label/" + sugReq.LabelName + "/values?" + values.Encode() + } else { + // Get Label names (keys) + newReq.Path = "/api/v1/labels" + newReq.URL = "/api/v1/labels?" + values.Encode() + } + + return r.Execute(ctx, newReq) +} diff --git a/pkg/registry/apis/alerting/notifications/receiver/authorize.go b/pkg/registry/apis/alerting/notifications/receiver/authorize.go index baf4bffd371..ce357351364 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/authorize.go +++ b/pkg/registry/apis/alerting/notifications/receiver/authorize.go @@ -53,9 +53,7 @@ func Authorize(ctx context.Context, ac AccessControlService, attr authorizer.Att return deny(err) } case "list": - if err := ac.AuthorizeReadSome(ctx, user); err != nil { // Preconditions, further checks are done downstream. - return deny(err) - } + return authorizer.DecisionAllow, "", nil // Always allow listing, receivers are filtered downstream. case "create": if err := ac.AuthorizeCreate(ctx, user); err != nil { return deny(err) diff --git a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go index 6f05581820c..79a97b640c9 100644 --- a/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go +++ b/pkg/registry/apis/alerting/notifications/receiver/legacy_storage.go @@ -2,9 +2,10 @@ package receiver import ( "context" + "errors" "fmt" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -14,6 +15,7 @@ import ( notifications "github.com/grafana/grafana/pkg/apis/alerting_notifications/v0alpha1" grafanaRest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + alertingac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" @@ -88,7 +90,14 @@ func (s *legacyStorage) List(ctx context.Context, opts *internalversion.ListOpti res, err := s.service.GetReceivers(ctx, q, user) if err != nil { - return nil, err + // This API should not be returning a forbidden error when the user does not have access to any resources. + // This can be true for a contact point creator role, for example. + // This should eventually be changed downstream in the auth logic but provisioning API currently relies on this + // behaviour to return useful forbidden errors when exporting decrypted receivers. + if !errors.Is(err, alertingac.ErrAuthorizationBase) { + return nil, err + } + res = nil } accesses, err := s.metadata.AccessControlMetadata(ctx, user, res...) @@ -112,7 +121,7 @@ func (s *legacyStorage) Get(ctx context.Context, uid string, _ *metav1.GetOption name, err := legacy_storage.UidToName(uid) if err != nil { - return nil, errors.NewNotFound(resourceInfo.GroupResource(), uid) + return nil, apierrors.NewNotFound(resourceInfo.GroupResource(), uid) } q := ngmodels.GetReceiverQuery{ OrgID: info.OrgID, @@ -172,7 +181,7 @@ func (s *legacyStorage) Create(ctx context.Context, return nil, fmt.Errorf("expected receiver but got %s", obj.GetObjectKind().GroupVersionKind()) } if p.ObjectMeta.Name != "" { // TODO remove when metadata.name can be defined by user - return nil, errors.NewBadRequest("object's metadata.name should be empty") + return nil, apierrors.NewBadRequest("object's metadata.name should be empty") } model, _, err := convertToDomainModel(p) if err != nil { @@ -271,5 +280,5 @@ func (s *legacyStorage) Delete(ctx context.Context, uid string, deleteValidation } func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) { - return nil, errors.NewMethodNotSupported(resourceInfo.GroupResource(), "deleteCollection") + return nil, apierrors.NewMethodNotSupported(resourceInfo.GroupResource(), "deleteCollection") } diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 1702c9c338e..5c56639efc2 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/playlist" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/scope" + "github.com/grafana/grafana/pkg/registry/apis/search" ) var ( @@ -37,6 +38,7 @@ func ProvideRegistryServiceSink( _ *scope.ScopeAPIBuilder, _ *query.QueryAPIBuilder, _ *notifications.NotificationsAPIBuilder, + _ *search.SearchAPIBuilder, ) *Service { return &Service{} } diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index 03cf09e225d..2f96178ca7f 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -246,6 +246,10 @@ func (a *dashboardSqlAccess) Read(ctx context.Context, req *resource.ReadRequest return a.ReadResource(ctx, req), nil } +func (a *dashboardSqlAccess) Search(ctx context.Context, req *resource.SearchRequest) (*resource.SearchResponse, error) { + return nil, fmt.Errorf("not yet (filter)") +} + func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryRequest) (*resource.HistoryResponse, error) { info, err := claims.ParseNamespace(req.Key.Namespace) if err == nil { diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 618a4c509d8..470d4baf287 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -47,8 +47,8 @@ func RegisterAPIService(cfg *setting.Cfg, accessControl accesscontrol.AccessControl, registerer prometheus.Registerer, ) *FolderAPIBuilder { - if !features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) { - return nil // skip registration unless opting into Kubernetes folders + if !features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) && !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs) { + return nil // skip registration unless opting into Kubernetes folders or unless we want to customise registration when testing } builder := &FolderAPIBuilder{ diff --git a/pkg/registry/apis/iam/authorizer.go b/pkg/registry/apis/iam/authorizer.go index 81dc1ee0187..8cc8cb68aba 100644 --- a/pkg/registry/apis/iam/authorizer.go +++ b/pkg/registry/apis/iam/authorizer.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/authlib/claims" "k8s.io/apiserver/pkg/authorization/authorizer" + "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -20,8 +21,8 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden Resource: iamv0.UserResourceInfo.GetName(), Attr: "id", Mapping: map[string]string{ - "get": accesscontrol.ActionOrgUsersRead, - "list": accesscontrol.ActionOrgUsersRead, + utils.VerbGet: accesscontrol.ActionOrgUsersRead, + utils.VerbList: accesscontrol.ActionOrgUsersRead, }, Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) { res, err := store.GetUserInternalID(ctx, ns, legacy.GetUserInternalIDQuery{ @@ -36,10 +37,23 @@ func newLegacyAuthorizer(ac accesscontrol.AccessControl, store legacy.LegacyIden accesscontrol.ResourceAuthorizerOptions{ Resource: "display", Unchecked: map[string]bool{ - "get": true, - "list": true, + utils.VerbGet: true, + utils.VerbList: true, }, }, + accesscontrol.ResourceAuthorizerOptions{ + Resource: iamv0.ServiceAccountResourceInfo.GetName(), + Attr: "id", + Resolver: accesscontrol.ResourceResolverFunc(func(ctx context.Context, ns claims.NamespaceInfo, name string) ([]string, error) { + res, err := store.GetServiceAccountInternalID(ctx, ns, legacy.GetServiceAccountInternalIDQuery{ + UID: name, + }) + if err != nil { + return nil, err + } + return []string{fmt.Sprintf("serviceaccounts:id:%d", res.ID)}, nil + }), + }, ) return gfauthorizer.NewResourceAuthorizer(client), client diff --git a/pkg/registry/apis/iam/common/common.go b/pkg/registry/apis/iam/common/common.go index 1e3533f3b73..ab6cf73bef5 100644 --- a/pkg/registry/apis/iam/common/common.go +++ b/pkg/registry/apis/iam/common/common.go @@ -1,9 +1,14 @@ package common import ( + "context" "strconv" + "github.com/grafana/authlib/claims" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/team" ) @@ -23,3 +28,92 @@ func MapTeamPermission(p team.PermissionType) iamv0.TeamPermission { return iamv0.TeamPermissionMember } } + +// Resource is required to be implemented for list return types so we can +// perform authorization. +type Resource interface { + AuthID() string +} + +type ListResponse[T Resource] struct { + Items []T + RV int64 + Continue int64 +} + +type ListFunc[T Resource] func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[T], error) + +// List is a helper function that will perform access check on resources if +// prvovided with a claims.AccessClient. +func List[T Resource]( + ctx context.Context, + resourceName string, + ac claims.AccessClient, + p Pagination, + fn ListFunc[T], +) (*ListResponse[T], error) { + ns, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + ident, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + check := func(_ string, _ string) bool { return true } + if ac != nil { + var err error + check, err = ac.Compile(ctx, ident, claims.AccessRequest{ + Verb: utils.VerbList, + Resource: resourceName, + Namespace: ns.Value, + }) + + if err != nil { + return nil, err + } + } + + res := &ListResponse[T]{Items: make([]T, 0, p.Limit)} + + first, err := fn(ctx, ns, p) + if err != nil { + return nil, err + } + + for _, item := range first.Items { + if !check(ns.Value, item.AuthID()) { + continue + } + res.Items = append(res.Items, item) + } + res.Continue = first.Continue + res.RV = first.RV + +outer: + for len(res.Items) < int(p.Limit) && res.Continue != 0 { + // FIXME: it is not optimal to reduce the amout we look for here but it is the easiest way to + // correctly handle pagination and continue tokens + r, err := fn(ctx, ns, Pagination{Limit: p.Limit - int64(len(res.Items)), Continue: res.Continue}) + if err != nil { + return nil, err + } + + for _, item := range r.Items { + if len(res.Items) == int(p.Limit) { + res.Continue = r.Continue + break outer + } + + if !check(ns.Value, item.AuthID()) { + continue + } + + res.Items = append(res.Items, item) + } + } + + return res, nil +} diff --git a/pkg/registry/apis/iam/common/common_test.go b/pkg/registry/apis/iam/common/common_test.go new file mode 100644 index 00000000000..c07c2cb63f9 --- /dev/null +++ b/pkg/registry/apis/iam/common/common_test.go @@ -0,0 +1,105 @@ +package common + +import ( + "context" + "testing" + + "github.com/grafana/authlib/claims" + "github.com/stretchr/testify/assert" + "k8s.io/apiserver/pkg/endpoints/request" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/featuremgmt" +) + +type item struct { + id string +} + +func (i item) AuthID() string { + return i.id +} + +func TestList(t *testing.T) { + ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) + + t.Run("should allow all items if no access client is passed", func(t *testing.T) { + ctx := newContext("stacks-1", newIdent()) + + res, err := List(ctx, "items", nil, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) { + return &ListResponse[item]{ + Items: []item{{"1"}, {"2"}}, + }, nil + }) + assert.NoError(t, err) + assert.Len(t, res.Items, 2) + }) + + t.Run("should filter out items that are allowed", func(t *testing.T) { + ctx := newContext("stacks-1", newIdent(accesscontrol.Permission{Action: "items:read", Scope: "items:uid:1"})) + + a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{ + Resource: "items", + Attr: "uid", + }) + res, err := List(ctx, "items", a, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) { + return &ListResponse[item]{ + Items: []item{{"1"}, {"2"}}, + }, nil + }) + assert.NoError(t, err) + assert.Len(t, res.Items, 1) + }) + + t.Run("should fetch more for partial response with continue token", func(t *testing.T) { + ctx := newContext("stacks-1", newIdent( + accesscontrol.Permission{Action: "items:read", Scope: "items:uid:1"}, + accesscontrol.Permission{Action: "items:read", Scope: "items:uid:3"}, + )) + + a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{ + Resource: "items", + Attr: "uid", + }) + + var called bool + + res, err := List(ctx, "items", a, Pagination{Limit: 2}, func(ctx context.Context, ns claims.NamespaceInfo, p Pagination) (*ListResponse[item], error) { + if called { + return &ListResponse[item]{ + Items: []item{{"3"}}, + }, nil + } + + called = true + return &ListResponse[item]{ + Items: []item{{"1"}, {"2"}}, + Continue: 3, + }, nil + }) + assert.NoError(t, err) + assert.Len(t, res.Items, 2) + + assert.Equal(t, "1", res.Items[0].AuthID()) + assert.Equal(t, "3", res.Items[1].AuthID()) + }) +} + +func newContext(namespace string, ident *identity.StaticRequester) context.Context { + return request.WithNamespace(identity.WithRequester(context.Background(), ident), namespace) +} + +func newIdent(permissions ...accesscontrol.Permission) *identity.StaticRequester { + pmap := map[string][]string{} + for _, p := range permissions { + pmap[p.Action] = append(pmap[p.Action], p.Scope) + } + + return &identity.StaticRequester{ + OrgID: 1, + Permissions: map[int64]map[string][]string{1: pmap}, + } +} diff --git a/pkg/registry/apis/iam/legacy/service_account.go b/pkg/registry/apis/iam/legacy/service_account.go index 03610e2a3f2..6e87be7f13d 100644 --- a/pkg/registry/apis/iam/legacy/service_account.go +++ b/pkg/registry/apis/iam/legacy/service_account.go @@ -2,6 +2,7 @@ package legacy import ( "context" + "errors" "fmt" "time" @@ -11,6 +12,83 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" ) +type GetServiceAccountInternalIDQuery struct { + OrgID int64 + UID string +} + +type GetServiceAccountInternalIDResult struct { + ID int64 +} + +var sqlQueryServiceAccountInternalIDTemplate = mustTemplate("service_account_internal_id.sql") + +func newGetServiceAccountInternalID(sql *legacysql.LegacyDatabaseHelper, q *GetServiceAccountInternalIDQuery) getServiceAccountInternalIDQuery { + return getServiceAccountInternalIDQuery{ + SQLTemplate: sqltemplate.New(sql.DialectForDriver()), + UserTable: sql.Table("user"), + OrgUserTable: sql.Table("org_user"), + Query: q, + } +} + +type getServiceAccountInternalIDQuery struct { + sqltemplate.SQLTemplate + UserTable string + OrgUserTable string + Query *GetServiceAccountInternalIDQuery +} + +func (r getServiceAccountInternalIDQuery) Validate() error { + return nil // TODO +} + +func (s *legacySQLStore) GetServiceAccountInternalID( + ctx context.Context, + ns claims.NamespaceInfo, + query GetServiceAccountInternalIDQuery, +) (*GetServiceAccountInternalIDResult, error) { + query.OrgID = ns.OrgID + if query.OrgID == 0 { + return nil, fmt.Errorf("expected non zero org id") + } + + sql, err := s.sql(ctx) + if err != nil { + return nil, err + } + + req := newGetServiceAccountInternalID(sql, &query) + q, err := sqltemplate.Execute(sqlQueryServiceAccountInternalIDTemplate, req) + if err != nil { + return nil, fmt.Errorf("execute template %q: %w", sqlQueryServiceAccountInternalIDTemplate.Name(), err) + } + + rows, err := sql.DB.GetSqlxSession().Query(ctx, q, req.GetArgs()...) + defer func() { + if rows != nil { + _ = rows.Close() + } + }() + + if err != nil { + return nil, err + } + + if !rows.Next() { + return nil, errors.New("service account not found") + } + + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + + return &GetServiceAccountInternalIDResult{ + id, + }, nil +} + type ListServiceAccountsQuery struct { UID string OrgID int64 diff --git a/pkg/registry/apis/iam/legacy/service_account_internal_id.sql b/pkg/registry/apis/iam/legacy/service_account_internal_id.sql new file mode 100644 index 00000000000..f6c468bf577 --- /dev/null +++ b/pkg/registry/apis/iam/legacy/service_account_internal_id.sql @@ -0,0 +1,7 @@ +SELECT u.id +FROM {{ .Ident .UserTable }} as u +INNER JOIN {{ .Ident .OrgUserTable }} as o ON u.id = o.user_id +WHERE o.org_id = {{ .Arg .Query.OrgID }} +AND u.uid = {{ .Arg .Query.UID }} +AND u.is_service_account +LIMIT 1; diff --git a/pkg/registry/apis/iam/legacy/sql.go b/pkg/registry/apis/iam/legacy/sql.go index 47b29e65e82..5e0e3f470ed 100644 --- a/pkg/registry/apis/iam/legacy/sql.go +++ b/pkg/registry/apis/iam/legacy/sql.go @@ -18,6 +18,7 @@ type LegacyIdentityStore interface { ListUsers(ctx context.Context, ns claims.NamespaceInfo, query ListUserQuery) (*ListUserResult, error) ListUserTeams(ctx context.Context, ns claims.NamespaceInfo, query ListUserTeamsQuery) (*ListUserTeamsResult, error) + GetServiceAccountInternalID(ctx context.Context, ns claims.NamespaceInfo, query GetServiceAccountInternalIDQuery) (*GetServiceAccountInternalIDResult, error) ListServiceAccounts(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountsQuery) (*ListServiceAccountResult, error) ListServiceAccountTokens(ctx context.Context, ns claims.NamespaceInfo, query ListServiceAccountTokenQuery) (*ListServiceAccountTokenResult, error) diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 46bbd06b920..1a69dcf136c 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -108,7 +108,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[userResource.StoragePath("teams")] = user.NewLegacyTeamMemberREST(b.store) serviceAccountResource := iamv0.ServiceAccountResourceInfo - storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store) + storage[serviceAccountResource.StoragePath()] = serviceaccount.NewLegacyStore(b.store, b.accessClient) storage[serviceAccountResource.StoragePath("tokens")] = serviceaccount.NewLegacyTokenREST(b.store) if b.sso != nil { diff --git a/pkg/registry/apis/iam/serviceaccount/store.go b/pkg/registry/apis/iam/serviceaccount/store.go index 618f2089793..fdc3e2f826e 100644 --- a/pkg/registry/apis/iam/serviceaccount/store.go +++ b/pkg/registry/apis/iam/serviceaccount/store.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" + "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" @@ -27,12 +28,13 @@ var ( var resource = iamv0.ServiceAccountResourceInfo -func NewLegacyStore(store legacy.LegacyIdentityStore) *LegacyStore { - return &LegacyStore{store} +func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient) *LegacyStore { + return &LegacyStore{store, ac} } type LegacyStore struct { store legacy.LegacyIdentityStore + ac claims.AccessClient } func (s *LegacyStore) New() runtime.Object { @@ -58,28 +60,38 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - ns, err := request.NamespaceInfoFrom(ctx, true) + res, err := common.List( + ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options), + func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.ServiceAccount], error) { + found, err := s.store.ListServiceAccounts(ctx, ns, legacy.ListServiceAccountsQuery{ + Pagination: p, + }) + + if err != nil { + return nil, err + } + + items := make([]iamv0.ServiceAccount, 0, len(found.Items)) + for _, sa := range found.Items { + items = append(items, toSAItem(sa, ns.Value)) + } + + return &common.ListResponse[iamv0.ServiceAccount]{ + Items: items, + RV: found.RV, + Continue: found.Continue, + }, nil + }, + ) + if err != nil { return nil, err } - found, err := s.store.ListServiceAccounts(ctx, ns, legacy.ListServiceAccountsQuery{ - OrgID: ns.OrgID, - Pagination: common.PaginationFromListOptions(options), - }) - if err != nil { - return nil, err - } - - list := &iamv0.ServiceAccountList{} - for _, item := range found.Items { - list.Items = append(list.Items, toSAItem(item, ns.Value)) - } - - list.ListMeta.Continue = common.OptionalFormatInt(found.Continue) - list.ListMeta.ResourceVersion = common.OptionalFormatInt(found.RV) - - return list, err + obj := &iamv0.ServiceAccountList{Items: res.Items} + obj.ListMeta.Continue = common.OptionalFormatInt(res.Continue) + obj.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV) + return obj, nil } func toSAItem(sa legacy.ServiceAccount, ns string) iamv0.ServiceAccount { diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go index 26104cf9638..8931df0ff69 100644 --- a/pkg/registry/apis/iam/user/store.go +++ b/pkg/registry/apis/iam/user/store.go @@ -11,7 +11,6 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "github.com/grafana/authlib/claims" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" iamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/iam/common" @@ -62,110 +61,51 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, } func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - ns, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } + res, err := common.List( + ctx, resource.GetName(), s.ac, common.PaginationFromListOptions(options), + func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0.User], error) { + found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ + Pagination: p, + }) - if s.ac != nil { - return s.listWithCheck(ctx, ns, common.PaginationFromListOptions(options)) - } - - return s.listWithoutCheck(ctx, ns, common.PaginationFromListOptions(options)) -} - -func (s *LegacyStore) listWithCheck(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (runtime.Object, error) { - ident, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - - check, err := s.ac.Compile(ctx, ident, claims.AccessRequest{ - Verb: "list", - Resource: resource.GetName(), - Namespace: ns.Value, - }) - - if err != nil { - return nil, err - } - - list := func(p common.Pagination) ([]iamv0.User, int64, int64, error) { - found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ - Pagination: p, - }) - - if err != nil { - return nil, 0, 0, err - } - - out := make([]iamv0.User, 0, len(found.Users)) - for _, u := range found.Users { - if check(ns.Value, strconv.FormatInt(u.ID, 10)) { - out = append(out, toUserItem(&u, ns.Value)) + if err != nil { + return nil, err } - } - return out, found.Continue, found.RV, nil - } + users := make([]iamv0.User, 0, len(found.Users)) + for _, u := range found.Users { + users = append(users, toUserItem(&u, ns.Value)) + } + + return &common.ListResponse[iamv0.User]{ + Items: users, + RV: found.RV, + Continue: found.Continue, + }, nil + }, + ) - items, c, rv, err := list(p) if err != nil { return nil, err } -outer: - for len(items) < int(p.Limit) && c != 0 { - var more []iamv0.User - more, c, _, err = list(common.Pagination{Limit: p.Limit, Continue: c}) - if err != nil { - return nil, err - } - - for _, u := range more { - if len(items) == int(p.Limit) { - break outer - } - items = append(items, u) - } - } - - obj := &iamv0.UserList{Items: items} - obj.ListMeta.Continue = common.OptionalFormatInt(c) - obj.ListMeta.ResourceVersion = common.OptionalFormatInt(rv) + obj := &iamv0.UserList{Items: res.Items} + obj.ListMeta.Continue = common.OptionalFormatInt(res.Continue) + obj.ListMeta.ResourceVersion = common.OptionalFormatInt(res.RV) return obj, nil } -func (s *LegacyStore) listWithoutCheck(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (runtime.Object, error) { - found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ - Pagination: p, - }) - - if err != nil { - return nil, err - } - - list := &iamv0.UserList{} - for _, item := range found.Users { - list.Items = append(list.Items, toUserItem(&item, ns.Value)) - } - - list.ListMeta.Continue = common.OptionalFormatInt(found.Continue) - list.ListMeta.ResourceVersion = common.OptionalFormatInt(found.RV) - return list, nil -} - func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { ns, err := request.NamespaceInfoFrom(ctx, true) if err != nil { return nil, err } - query := legacy.ListUserQuery{ - OrgID: ns.OrgID, - Pagination: common.Pagination{Limit: 1}, - } - found, err := s.store.ListUsers(ctx, ns, query) + found, err := s.store.ListUsers(ctx, ns, legacy.ListUserQuery{ + OrgID: ns.OrgID, + UID: name, + Pagination: common.Pagination{Limit: 1}, + }) if found == nil || err != nil { return nil, resource.NewNotFound(name) } @@ -191,6 +131,7 @@ func toUserItem(u *user.User, ns string) iamv0.User { Email: u.Email, EmailVerified: u.EmailVerified, Disabled: u.IsDisabled, + InternalID: u.ID, }, } obj, _ := utils.MetaAccessor(item) diff --git a/pkg/registry/apis/search/register.go b/pkg/registry/apis/search/register.go new file mode 100644 index 00000000000..78b2afbfd07 --- /dev/null +++ b/pkg/registry/apis/search/register.go @@ -0,0 +1,103 @@ +package search + +import ( + "encoding/json" + "net/http" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/registry/generic" + genericapiserver "k8s.io/apiserver/pkg/server" + common "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/spec3" + + grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" + "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/storage/unified/resource" +) + +var _ builder.APIGroupBuilder = (*SearchAPIBuilder)(nil) + +type SearchAPIBuilder struct { + unified resource.ResourceClient +} + +func NewSearchAPIBuilder( + unified resource.ResourceClient, +) (*SearchAPIBuilder, error) { + return &SearchAPIBuilder{ + unified: unified, + }, nil +} + +func RegisterAPIService( + features featuremgmt.FeatureToggles, + apiregistration builder.APIRegistrar, + unified resource.ResourceClient, +) (*SearchAPIBuilder, error) { + if !(features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) || features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs)) { + return nil, nil + } + builder, err := NewSearchAPIBuilder(unified) + apiregistration.RegisterAPI(builder) + return builder, err +} + +func (b *SearchAPIBuilder) GetGroupVersion() schema.GroupVersion { + return schema.GroupVersion{Group: "search.grafana.app", Version: "v0alpha1"} +} + +func (b *SearchAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { + return nil +} + +func (b *SearchAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { + return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { + return map[string]common.OpenAPIDefinition{} + } +} + +func (b *SearchAPIBuilder) GetAPIRoutes() *builder.APIRoutes { + return &builder.APIRoutes{ + Namespace: []builder.APIRouteHandler{ + { + Path: "search", + Spec: &spec3.PathProps{ + Get: &spec3.Operation{ + OperationProps: spec3.OperationProps{ + Tags: []string{"Search"}, + Summary: "Search", + Description: "Search for resources", + }, + }, + }, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + urlQuery := r.URL.Query().Get("query") + searchRequest := &resource.SearchRequest{Query: urlQuery} + res, err := b.unified.Search(r.Context(), searchRequest) + if err != nil { + panic(err) + } + if err := json.NewEncoder(w).Encode(res); err != nil { + panic(err) + } + }), + }, + }, + } +} + +func (b *SearchAPIBuilder) GetAuthorizer() authorizer.Authorizer { + return nil +} + +func (b *SearchAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { + return oas, nil +} + +func (b *SearchAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter, dualWriteBuilder grafanarest.DualWriteBuilder) error { + apiGroupInfo.PrioritizedVersions = []schema.GroupVersion{b.GetGroupVersion()} + return nil +} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index e9d3ac6adbf..0cd14526b5d 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/playlist" "github.com/grafana/grafana/pkg/registry/apis/query" "github.com/grafana/grafana/pkg/registry/apis/scope" + "github.com/grafana/grafana/pkg/registry/apis/search" "github.com/grafana/grafana/pkg/registry/apis/service" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" ) @@ -40,4 +41,5 @@ var WireSet = wire.NewSet( scope.RegisterAPIService, notifications.RegisterAPIService, //sso.RegisterAPIService, + search.RegisterAPIService, ) diff --git a/pkg/semconv/go.mod b/pkg/semconv/go.mod index a8a19c15a1c..cacc8f096dd 100644 --- a/pkg/semconv/go.mod +++ b/pkg/semconv/go.mod @@ -2,7 +2,7 @@ module github.com/grafana/grafana/pkg/semconv go 1.23.1 -require go.opentelemetry.io/otel v1.29.0 +require go.opentelemetry.io/otel v1.30.0 require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/pkg/semconv/go.sum b/pkg/semconv/go.sum index bf61a1563d6..0b9bd1ddf2b 100644 --- a/pkg/semconv/go.sum +++ b/pkg/semconv/go.sum @@ -6,7 +6,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= -go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 81574ec9677..5549a7d5966 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -295,6 +295,8 @@ var wireBasicSet = wire.NewSet( dashboardservice.ProvideDashboardPluginService, dashboardstore.ProvideDashboardStore, folderimpl.ProvideService, + folderimpl.ProvideStore, + wire.Bind(new(folder.Store), new(*folderimpl.FolderStoreImpl)), folderimpl.ProvideDashboardFolderStore, wire.Bind(new(folder.FolderStore), new(*folderimpl.DashboardFolderStoreImpl)), dashboardimportservice.ProvideService, @@ -465,5 +467,5 @@ func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOp // Initialize the standalone APIServer factory func InitializeAPIServerFactory() (standalone.APIServerFactory, error) { wire.Build(wireExtsStandaloneAPIServerSet) - return &standalone.DummyAPIFactory{}, nil // Wire will replace this with a real interface + return &standalone.NoOpAPIServerFactory{}, nil // Wire will replace this with a real interface } diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index 564aad51ea5..3d97c42aafd 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -141,5 +141,5 @@ var wireExtsModuleServerSet = wire.NewSet( ) var wireExtsStandaloneAPIServerSet = wire.NewSet( - standalone.GetDummyAPIFactory, + standalone.ProvideAPIServerFactory, ) diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 50616a33d3f..f9300fc1545 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -735,7 +735,7 @@ func (s *Service) SaveExternalServiceRole(ctx context.Context, cmd accesscontrol ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.SaveExternalServiceRole") defer span.End() - if !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { s.log.Debug("Registering an external service role is behind a feature flag, enable it to use this feature.") return nil } @@ -751,7 +751,7 @@ func (s *Service) DeleteExternalServiceRole(ctx context.Context, externalService ctx, span := tracer.Start(ctx, "accesscontrol.acimpl.DeleteExternalServiceRole") defer span.End() - if !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !s.cfg.ManagedServiceAccountsEnabled || !s.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { s.log.Debug("Deleting an external service role is behind a feature flag, enable it to use this feature.") return nil } diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index 7096b2fcac0..4c1dc562d8b 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -900,6 +900,7 @@ func TestService_SaveExternalServiceRole(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() ac := setupTestEnv(t) + ac.cfg.ManagedServiceAccountsEnabled = true ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts) for _, r := range tt.runs { err := ac.SaveExternalServiceRole(ctx, r.cmd) @@ -946,6 +947,7 @@ func TestService_DeleteExternalServiceRole(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() ac := setupTestEnv(t) + ac.cfg.ManagedServiceAccountsEnabled = true ac.features = featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts) if tt.initCmd != nil { diff --git a/pkg/services/accesscontrol/authorizer.go b/pkg/services/accesscontrol/authorizer.go index 9134166f0f3..5d400b06dc9 100644 --- a/pkg/services/accesscontrol/authorizer.go +++ b/pkg/services/accesscontrol/authorizer.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" ) // ResourceResolver is called before authorization is performed. @@ -35,6 +36,7 @@ type ResourceAuthorizerOptions struct { Attr string // Mapping is used to translate k8s verb to rbac action. // Key is the desired verb and value the rbac action it should be translated into. + // If no mapping is provided it will get a "default" mapping. Mapping map[string]string // Resolver if passed can translate into one or more scopes used to authorize resource. // This is useful when stored scopes are based on something else than k8s name or @@ -47,13 +49,28 @@ var _ claims.AccessClient = (*LegacyAccessClient)(nil) func NewLegacyAccessClient(ac AccessControl, opts ...ResourceAuthorizerOptions) *LegacyAccessClient { stored := map[string]ResourceAuthorizerOptions{} + defaultMapping := func(r string) map[string]string { + return map[string]string{ + utils.VerbGet: fmt.Sprintf("%s:read", r), + utils.VerbList: fmt.Sprintf("%s:read", r), + utils.VerbWatch: fmt.Sprintf("%s:read", r), + utils.VerbCreate: fmt.Sprintf("%s:create", r), + utils.VerbUpdate: fmt.Sprintf("%s:write", r), + utils.VerbPatch: fmt.Sprintf("%s:write", r), + utils.VerbDelete: fmt.Sprintf("%s:delete", r), + utils.VerbDeleteCollection: fmt.Sprintf("%s:delete", r), + } + } + for _, o := range opts { if o.Unchecked == nil { o.Unchecked = map[string]bool{} } + if o.Mapping == nil { - o.Mapping = map[string]string{} + o.Mapping = defaultMapping(o.Resource) } + stored[o.Resource] = o } @@ -107,7 +124,7 @@ func (c *LegacyAccessClient) HasAccess(ctx context.Context, id claims.AuthInfo, } else { eval = EvalPermission(action, fmt.Sprintf("%s:%s:%s", opts.Resource, opts.Attr, req.Name)) } - } else if req.Verb == "list" { + } else if req.Verb == utils.VerbList { // For list request we need to filter out in storage layer. eval = EvalPermission(action) } else { diff --git a/pkg/services/accesscontrol/authorizer_test.go b/pkg/services/accesscontrol/authorizer_test.go index d602d105cf2..5d7e7f28f71 100644 --- a/pkg/services/accesscontrol/authorizer_test.go +++ b/pkg/services/accesscontrol/authorizer_test.go @@ -14,14 +14,11 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" ) -func TestResourceAuthorizer_HasAccess(t *testing.T) { +func TestLegacyAccessClient_HasAccess(t *testing.T) { ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) - t.Run("should have no opinion for non resource requests", func(t *testing.T) { - a := accesscontrol.NewLegacyAccessClient(ac, accesscontrol.ResourceAuthorizerOptions{ - Resource: "dashboards", - Attr: "uid", - }) + t.Run("should reject when when no configuration for resource exist", func(t *testing.T) { + a := accesscontrol.NewLegacyAccessClient(ac) ok, err := a.HasAccess(context.Background(), &identity.StaticRequester{}, claims.AccessRequest{ Verb: "get", @@ -29,7 +26,7 @@ func TestResourceAuthorizer_HasAccess(t *testing.T) { Namespace: "default", Name: "1", }) - assert.Error(t, err) + assert.NoError(t, err) assert.Equal(t, false, ok) }) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go index 85e5d5a4960..8010ef19c27 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/dashboard.go @@ -93,7 +93,7 @@ func registerDashboardRoles(cfg *setting.Cfg, features featuremgmt.FeatureToggle func ProvideDashboardPermissions( cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, folderStore folder.Store, service accesscontrol.Service, teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService, ) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { @@ -145,7 +145,7 @@ func ProvideDashboardPermissions( } parentScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(queryResult.UID) - nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderService) + nestedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, queryResult.UID, folderStore) if err != nil { return nil, err } diff --git a/pkg/services/accesscontrol/ossaccesscontrol/folder.go b/pkg/services/accesscontrol/ossaccesscontrol/folder.go index 8904567d935..1af283a81c2 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/folder.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/folder.go @@ -84,7 +84,7 @@ func registerFolderRoles(cfg *setting.Cfg, features featuremgmt.FeatureToggles, func ProvideFolderPermissions( cfg *setting.Cfg, features featuremgmt.FeatureToggles, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl, - license licensing.Licensing, dashboardStore dashboards.Store, folderService folder.Service, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, folderStore folder.Store, service accesscontrol.Service, teamService team.Service, userService user.Service, actionSetService resourcepermissions.ActionSetService, ) (*FolderPermissionsService, error) { if err := registerFolderRoles(cfg, features, service); err != nil { @@ -111,7 +111,7 @@ func ProvideFolderPermissions( return nil }, InheritedScopesSolver: func(ctx context.Context, orgID int64, resourceID string) ([]string, error) { - return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderService) + return dashboards.GetInheritedScopes(ctx, orgID, resourceID, folderStore) }, Assignments: resourcepermissions.Assignments{ Users: true, diff --git a/pkg/services/accesscontrol/ossaccesscontrol/receivers.go b/pkg/services/accesscontrol/ossaccesscontrol/receivers.go index ac5f5f6d33c..a818d8b3e84 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/receivers.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/receivers.go @@ -133,7 +133,14 @@ func (r ReceiverPermissionsService) CopyPermissions(ctx context.Context, orgID i // Clear permission cache for the user who updated the receiver, so that new permissions are fetched for their next call // Required for cases when caller wants to immediately interact with the newly updated object if user != nil && user.IsIdentityType(claims.TypeUser) { - r.ac.ClearUserPermissionCache(user) + // A more comprehensive means of clearing the user's permissions cache than ClearUserPermissionCache. + // It also clears the cache for basic roles and teams, which is required for the user to not have temporarily + // broken UI permissions when their source of elevated permissions comes from a cached team or basic role + // permission. + _, err = r.ac.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: true}) + if err != nil { + r.log.Debug("Failed to clear user permissions cache", "error", err) + } } return countCustomPermissions(setPermissionCommands), nil diff --git a/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go new file mode 100644 index 00000000000..9dbed99aa67 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/testutil/testutil.go @@ -0,0 +1,83 @@ +package testutil + +import ( + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/permreg" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder/folderimpl" + "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/sqlstore" + "github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry" + "github.com/grafana/grafana/pkg/services/team/teamimpl" + "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/setting" +) + +func ProvideFolderPermissions( + features featuremgmt.FeatureToggles, + cfg *setting.Cfg, + sqlStore *sqlstore.SQLStore, +) (*ossaccesscontrol.FolderPermissionsService, error) { + actionSets := resourcepermissions.NewActionSetService(features) + acSvc := acimpl.ProvideOSSService( + cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(), + features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sqlStore, permreg.ProvidePermissionRegistry(), + ) + + license := licensingtest.NewFakeLicensing() + license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() + + ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) + + fStore := folderimpl.ProvideStore(sqlStore) + + quotaService := quotatest.New(false, nil) + orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService) + if err != nil { + return nil, err + } + teamSvc, err := teamimpl.ProvideService(sqlStore, cfg, tracing.InitializeTracerForTest()) + if err != nil { + return nil, err + } + cache := localcache.ProvideService() + + userSvc, err := userimpl.ProvideService( + sqlStore, + orgService, + cfg, + teamSvc, + cache, + tracing.InitializeTracerForTest(), + quotaService, + bundleregistry.ProvideService(), + ) + if err != nil { + return nil, err + } + + return ossaccesscontrol.ProvideFolderPermissions( + cfg, + features, + routing.NewRouteRegister(), + sqlStore, + ac, + license, + &dashboards.FakeDashboardStore{}, + fStore, + acSvc, + teamSvc, + userSvc, + actionSets, + ) +} diff --git a/pkg/services/annotations/accesscontrol/accesscontrol.go b/pkg/services/annotations/accesscontrol/accesscontrol.go index adf8fe8401d..cc2f7e980bd 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol.go @@ -39,7 +39,7 @@ func NewAuthService(db db.DB, features featuremgmt.FeatureToggles) *AuthService } // Authorize checks if the user has permission to read annotations, then returns a struct containing dashboards and scope types that the user has access to. -func (authz *AuthService) Authorize(ctx context.Context, query *annotations.ItemQuery) (*AccessResources, error) { +func (authz *AuthService) Authorize(ctx context.Context, query annotations.ItemQuery) (*AccessResources, error) { user := query.SignedInUser if user == nil || user.IsNil() { return nil, ErrReadForbidden.Errorf("missing user") @@ -80,7 +80,7 @@ func (authz *AuthService) Authorize(ctx context.Context, query *annotations.Item }, nil } -func (authz *AuthService) getAnnotationDashboard(ctx context.Context, query *annotations.ItemQuery) (int64, error) { +func (authz *AuthService) getAnnotationDashboard(ctx context.Context, query annotations.ItemQuery) (int64, error) { var items []annotations.Item params := make([]any, 0) err := authz.db.WithDbSession(ctx, func(sess *db.Session) error { @@ -106,7 +106,7 @@ func (authz *AuthService) getAnnotationDashboard(ctx context.Context, query *ann return items[0].DashboardID, nil } -func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context, query *annotations.ItemQuery) (map[string]int64, error) { +func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context, query annotations.ItemQuery) (map[string]int64, error) { recursiveQueriesSupported, err := authz.db.RecursiveQueriesAreSupported() if err != nil { return nil, err diff --git a/pkg/services/annotations/accesscontrol/accesscontrol_test.go b/pkg/services/annotations/accesscontrol/accesscontrol_test.go index 42f767bec35..f3d82853b7c 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol_test.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol_test.go @@ -175,7 +175,7 @@ func TestIntegrationAuthorize(t *testing.T) { authz := NewAuthService(sql, featuremgmt.WithFeatures(tc.featureToggle)) - query := &annotations.ItemQuery{SignedInUser: u, OrgID: 1} + query := annotations.ItemQuery{SignedInUser: u, OrgID: 1} resources, err := authz.Authorize(context.Background(), query) require.NoError(t, err) diff --git a/pkg/services/annotations/annotationsimpl/annotations.go b/pkg/services/annotations/annotationsimpl/annotations.go index da0e960487a..22c0ec796bb 100644 --- a/pkg/services/annotations/annotationsimpl/annotations.go +++ b/pkg/services/annotations/annotationsimpl/annotations.go @@ -79,7 +79,7 @@ func (r *RepositoryImpl) Find(ctx context.Context, query *annotations.ItemQuery) // Search without dashboard UID filter is expensive, so check without access control first if query.DashboardID == 0 && query.DashboardUID == "" { // Return early if no annotations found, it's not necessary to perform expensive access control filtering - res, err := r.reader.Get(ctx, query, &accesscontrol.AccessResources{ + res, err := r.reader.Get(ctx, *query, &accesscontrol.AccessResources{ SkipAccessControlFilter: true, }) if err != nil || len(res) == 0 { @@ -97,12 +97,12 @@ func (r *RepositoryImpl) Find(ctx context.Context, query *annotations.ItemQuery) // Iterate over available annotations until query limit is reached // or all available dashboards are checked for len(results) < int(query.Limit) { - resources, err := r.authZ.Authorize(ctx, query) + resources, err := r.authZ.Authorize(ctx, *query) if err != nil { return nil, err } - res, err := r.reader.Get(ctx, query, resources) + res, err := r.reader.Get(ctx, *query, resources) if err != nil { return nil, err } @@ -123,5 +123,5 @@ func (r *RepositoryImpl) Delete(ctx context.Context, params *annotations.DeleteP } func (r *RepositoryImpl) FindTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) { - return r.reader.GetTags(ctx, query) + return r.reader.GetTags(ctx, *query) } diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index 7a1223bfb64..6a1f9e56a2f 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + ftestutil "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/testutil" "github.com/grafana/grafana/pkg/services/authz/zanzana" @@ -227,8 +228,11 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { }) ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()) - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(sql), sql, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - + folderPermissions, err := ftestutil.ProvideFolderPermissions(features, cfg, sql) + require.NoError(t, err) + fStore := folderimpl.ProvideStore(sql) + folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, + folderimpl.ProvideDashboardFolderStore(sql), sql, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) cfg.AnnotationMaximumTagsLength = 60 store := NewXormStore(cfg, log.New("annotation.test"), sql, tagService) diff --git a/pkg/services/annotations/annotationsimpl/composite_store.go b/pkg/services/annotations/annotationsimpl/composite_store.go index bc59855c61c..2c1b83e5e10 100644 --- a/pkg/services/annotations/annotationsimpl/composite_store.go +++ b/pkg/services/annotations/annotationsimpl/composite_store.go @@ -32,7 +32,7 @@ func (c *CompositeStore) Type() string { } // Get returns annotations from all stores, and combines the results. -func (c *CompositeStore) Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { +func (c *CompositeStore) Get(ctx context.Context, query annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { itemCh := make(chan []*annotations.ItemDTO, len(c.readers)) err := concurrency.ForEachJob(ctx, len(c.readers), len(c.readers), func(ctx context.Context, i int) (err error) { @@ -57,7 +57,7 @@ func (c *CompositeStore) Get(ctx context.Context, query *annotations.ItemQuery, } // GetTags returns tags from all stores, and combines the results. -func (c *CompositeStore) GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) { +func (c *CompositeStore) GetTags(ctx context.Context, query annotations.TagsQuery) (annotations.FindTagsResult, error) { resCh := make(chan annotations.FindTagsResult, len(c.readers)) err := concurrency.ForEachJob(ctx, len(c.readers), len(c.readers), func(ctx context.Context, i int) (err error) { diff --git a/pkg/services/annotations/annotationsimpl/composite_store_test.go b/pkg/services/annotations/annotationsimpl/composite_store_test.go index b5aa1d22d8c..c916fc3eaa0 100644 --- a/pkg/services/annotations/annotationsimpl/composite_store_test.go +++ b/pkg/services/annotations/annotationsimpl/composite_store_test.go @@ -7,10 +7,11 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/accesscontrol" - "github.com/stretchr/testify/require" ) var ( @@ -21,7 +22,7 @@ var ( func TestCompositeStore(t *testing.T) { t.Run("should handle panic", func(t *testing.T) { r1 := newFakeReader() - getPanic := func(context.Context, *annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { + getPanic := func(context.Context, annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { panic("ohno") } r2 := newFakeReader(withGetFn(getPanic)) @@ -30,7 +31,7 @@ func TestCompositeStore(t *testing.T) { []readStore{r1, r2}, } - _, err := store.Get(context.Background(), nil, nil) + _, err := store.Get(context.Background(), annotations.ItemQuery{}, nil) require.Error(t, err) require.Contains(t, err.Error(), "concurrent job panic") }) @@ -51,11 +52,11 @@ func TestCompositeStore(t *testing.T) { err error }{ { - f: func() (any, error) { return store.Get(context.Background(), nil, nil) }, + f: func() (any, error) { return store.Get(context.Background(), annotations.ItemQuery{}, nil) }, err: errGet, }, { - f: func() (any, error) { return store.GetTags(context.Background(), nil) }, + f: func() (any, error) { return store.GetTags(context.Background(), annotations.TagsQuery{}) }, err: errGetTags, }, } @@ -93,7 +94,7 @@ func TestCompositeStore(t *testing.T) { {TimeEnd: 1, Time: 1}, } - items, _ := store.Get(context.Background(), nil, nil) + items, _ := store.Get(context.Background(), annotations.ItemQuery{}, nil) require.Equal(t, expected, items) }) @@ -122,16 +123,40 @@ func TestCompositeStore(t *testing.T) { {Tag: "key2:val2"}, } - res, _ := store.GetTags(context.Background(), nil) + res, _ := store.GetTags(context.Background(), annotations.TagsQuery{}) require.Equal(t, expected, res.Tags) }) + + // Check if reader is not modifying query since it might cause a race condition in case of composite store + t.Run("should not modify query", func(t *testing.T) { + getFn1 := func(ctx context.Context, query annotations.ItemQuery, resources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { + query.From = 1 + return []*annotations.ItemDTO{}, nil + } + getFn2 := func(ctx context.Context, query annotations.ItemQuery, resources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { + return []*annotations.ItemDTO{}, nil + } + + r1 := newFakeReader(withGetFn(getFn1)) + r2 := newFakeReader(withGetFn(getFn2)) + + store := &CompositeStore{ + log.NewNopLogger(), + []readStore{r1, r2}, + } + + query := annotations.ItemQuery{} + _, err := store.Get(context.Background(), query, nil) + require.NoError(t, err) + require.Equal(t, int64(0), query.From) + }) } type fakeReader struct { items []*annotations.ItemDTO tagRes annotations.FindTagsResult - getFn func(context.Context, *annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) - getTagFn func(context.Context, *annotations.TagsQuery) (annotations.FindTagsResult, error) + getFn func(context.Context, annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) + getTagFn func(context.Context, annotations.TagsQuery) (annotations.FindTagsResult, error) wait time.Duration err error } @@ -140,7 +165,7 @@ func (f *fakeReader) Type() string { return "fake" } -func (f *fakeReader) Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { +func (f *fakeReader) Get(ctx context.Context, query annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { if f.getFn != nil { return f.getFn(ctx, query, accessResources) } @@ -157,7 +182,7 @@ func (f *fakeReader) Get(ctx context.Context, query *annotations.ItemQuery, acce return f.items, nil } -func (f *fakeReader) GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) { +func (f *fakeReader) GetTags(ctx context.Context, query annotations.TagsQuery) (annotations.FindTagsResult, error) { if f.getTagFn != nil { return f.getTagFn(ctx, query) } @@ -198,7 +223,7 @@ func withTags(tags []*annotations.TagsDTO) func(*fakeReader) { } } -func withGetFn(fn func(context.Context, *annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error)) func(*fakeReader) { +func withGetFn(fn func(context.Context, annotations.ItemQuery, *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error)) func(*fakeReader) { return func(f *fakeReader) { f.getFn = fn } diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store.go b/pkg/services/annotations/annotationsimpl/loki/historian_store.go index 38b47f2f726..563d4435be7 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store.go @@ -80,7 +80,7 @@ func (r *LokiHistorianStore) Type() string { return "loki" } -func (r *LokiHistorianStore) Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { +func (r *LokiHistorianStore) Get(ctx context.Context, query annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { if query.Type == "annotation" { return make([]*annotations.ItemDTO, 0), nil } @@ -104,7 +104,7 @@ func (r *LokiHistorianStore) Get(ctx context.Context, query *annotations.ItemQue } // No folders in the filter because it filter by Dashboard UID, and the request is already authorized. - logQL, err := historian.BuildLogQuery(buildHistoryQuery(query, accessResources.Dashboards, rule.UID), nil, r.client.MaxQuerySize()) + logQL, err := historian.BuildLogQuery(buildHistoryQuery(&query, accessResources.Dashboards, rule.UID), nil, r.client.MaxQuerySize()) if err != nil { grafanaErr := errutil.Error{} if errors.As(err, &grafanaErr) { @@ -192,7 +192,7 @@ func (r *LokiHistorianStore) annotationsFromStream(stream historian.Stream, ac a return items } -func (r *LokiHistorianStore) GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) { +func (r *LokiHistorianStore) GetTags(ctx context.Context, query annotations.TagsQuery) (annotations.FindTagsResult, error) { return annotations.FindTagsResult{Tags: []*annotations.TagsDTO{}}, nil } diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go index bcd5e536ae0..242dbcea396 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go @@ -96,7 +96,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { } res, err := store.Get( context.Background(), - &query, + query, &annotation_ac.AccessResources{ Dashboards: map[string]int64{ dashboard1.UID: dashboard1.ID, @@ -126,7 +126,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { } _, err := store.Get( context.Background(), - &query, + query, &annotation_ac.AccessResources{ Dashboards: map[string]int64{ dashboard1.UID: dashboard1.ID, @@ -151,7 +151,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { } res, err := store.Get( context.Background(), - &query, + query, &annotation_ac.AccessResources{ Dashboards: map[string]int64{ dashboard1.UID: dashboard1.ID, @@ -175,7 +175,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { } res, err := store.Get( context.Background(), - &query, + query, &annotation_ac.AccessResources{ Dashboards: map[string]int64{ dashboard1.UID: dashboard1.ID, @@ -201,7 +201,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { } res, err := store.Get( context.Background(), - &query, + query, &annotation_ac.AccessResources{ Dashboards: map[string]int64{ dashboard1.UID: dashboard1.ID, @@ -231,7 +231,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { } res, err := store.Get( context.Background(), - &query, + query, &annotation_ac.AccessResources{ Dashboards: map[string]int64{ dashboard1.UID: dashboard1.ID, @@ -260,7 +260,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { } res, err := store.Get( context.Background(), - &query, + query, &annotation_ac.AccessResources{ Dashboards: map[string]int64{ dashboard1.UID: dashboard1.ID, @@ -294,7 +294,7 @@ func TestIntegrationAlertStateHistoryStore(t *testing.T) { } res, err := store.Get( context.Background(), - &query, + query, &annotation_ac.AccessResources{ Dashboards: map[string]int64{ dashboard1.UID: dashboard1.ID, diff --git a/pkg/services/annotations/annotationsimpl/store.go b/pkg/services/annotations/annotationsimpl/store.go index b1c3643a9c3..9210a2c8b96 100644 --- a/pkg/services/annotations/annotationsimpl/store.go +++ b/pkg/services/annotations/annotationsimpl/store.go @@ -20,8 +20,8 @@ type commonStore interface { type readStore interface { commonStore - Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) - GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) + Get(ctx context.Context, query annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) + GetTags(ctx context.Context, query annotations.TagsQuery) (annotations.FindTagsResult, error) } type writeStore interface { diff --git a/pkg/services/annotations/annotationsimpl/xorm_store.go b/pkg/services/annotations/annotationsimpl/xorm_store.go index 71c9a44984d..603ec3865e8 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store.go @@ -245,7 +245,7 @@ func tagSet[T any](fn func(T) int64, list []T) map[int64]struct{} { return set } -func (r *xormRepositoryImpl) Get(ctx context.Context, query *annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { +func (r *xormRepositoryImpl) Get(ctx context.Context, query annotations.ItemQuery, accessResources *accesscontrol.AccessResources) ([]*annotations.ItemDTO, error) { var sql bytes.Buffer params := make([]interface{}, 0) items := make([]*annotations.ItemDTO, 0) @@ -446,7 +446,7 @@ func (r *xormRepositoryImpl) Delete(ctx context.Context, params *annotations.Del }) } -func (r *xormRepositoryImpl) GetTags(ctx context.Context, query *annotations.TagsQuery) (annotations.FindTagsResult, error) { +func (r *xormRepositoryImpl) GetTags(ctx context.Context, query annotations.TagsQuery) (annotations.FindTagsResult, error) { var items []*annotations.Tag err := r.db.WithDbSession(ctx, func(dbSession *db.Session) error { if query.Limit == 0 { diff --git a/pkg/services/annotations/annotationsimpl/xorm_store_test.go b/pkg/services/annotations/annotationsimpl/xorm_store_test.go index ae06428f835..d72e61e3b13 100644 --- a/pkg/services/annotations/annotationsimpl/xorm_store_test.go +++ b/pkg/services/annotations/annotationsimpl/xorm_store_test.go @@ -133,7 +133,7 @@ func TestIntegrationAnnotations(t *testing.T) { assert.Greater(t, organizationAnnotation2.ID, int64(0)) t.Run("Can query for annotation by dashboard id", func(t *testing.T) { - items, err := store.Get(context.Background(), &annotations.ItemQuery{ + items, err := store.Get(context.Background(), annotations.ItemQuery{ OrgID: 1, DashboardID: dashboard.ID, From: 0, @@ -182,7 +182,7 @@ func TestIntegrationAnnotations(t *testing.T) { err := store.AddMany(context.Background(), items) require.NoError(t, err) - query := &annotations.ItemQuery{OrgID: 100, SignedInUser: testUser} + query := annotations.ItemQuery{OrgID: 100, SignedInUser: testUser} accRes := &annotation_ac.AccessResources{CanAccessOrgAnnotations: true} inserted, err := store.Get(context.Background(), query, accRes) require.NoError(t, err) @@ -209,7 +209,7 @@ func TestIntegrationAnnotations(t *testing.T) { err := store.AddMany(context.Background(), items) require.NoError(t, err) - query := &annotations.ItemQuery{OrgID: 101, SignedInUser: testUser} + query := annotations.ItemQuery{OrgID: 101, SignedInUser: testUser} accRes := &annotation_ac.AccessResources{CanAccessOrgAnnotations: true} inserted, err := store.Get(context.Background(), query, accRes) require.NoError(t, err) @@ -217,7 +217,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Can query for annotation by id", func(t *testing.T) { - items, err := store.Get(context.Background(), &annotations.ItemQuery{ + items, err := store.Get(context.Background(), annotations.ItemQuery{ OrgID: 1, AnnotationID: annotation2.ID, SignedInUser: testUser, @@ -237,7 +237,7 @@ func TestIntegrationAnnotations(t *testing.T) { Dashboards: map[string]int64{"foo": 1}, CanAccessDashAnnotations: true, } - items, err := store.Get(context.Background(), &annotations.ItemQuery{ + items, err := store.Get(context.Background(), annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 12, @@ -253,7 +253,7 @@ func TestIntegrationAnnotations(t *testing.T) { Dashboards: map[string]int64{"foo": 1}, CanAccessDashAnnotations: true, } - items, err := store.Get(context.Background(), &annotations.ItemQuery{ + items, err := store.Get(context.Background(), annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 1, @@ -270,7 +270,7 @@ func TestIntegrationAnnotations(t *testing.T) { Dashboards: map[string]int64{"foo": 1}, CanAccessDashAnnotations: true, } - items, err := store.Get(context.Background(), &annotations.ItemQuery{ + items, err := store.Get(context.Background(), annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 1, @@ -287,7 +287,7 @@ func TestIntegrationAnnotations(t *testing.T) { Dashboards: map[string]int64{"foo": 1}, CanAccessDashAnnotations: true, } - items, err := store.Get(context.Background(), &annotations.ItemQuery{ + items, err := store.Get(context.Background(), annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 1, @@ -301,7 +301,7 @@ func TestIntegrationAnnotations(t *testing.T) { t.Run("Should find two annotations using partial match", func(t *testing.T) { accRes := &annotation_ac.AccessResources{CanAccessOrgAnnotations: true} - items, err := store.Get(context.Background(), &annotations.ItemQuery{ + items, err := store.Get(context.Background(), annotations.ItemQuery{ OrgID: 1, From: 1, To: 25, @@ -318,7 +318,7 @@ func TestIntegrationAnnotations(t *testing.T) { Dashboards: map[string]int64{"foo": 1}, CanAccessDashAnnotations: true, } - items, err := store.Get(context.Background(), &annotations.ItemQuery{ + items, err := store.Get(context.Background(), annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 1, @@ -331,7 +331,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Can update annotation and remove all tags", func(t *testing.T) { - query := &annotations.ItemQuery{ + query := annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 0, @@ -366,7 +366,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Can update annotation with new tags", func(t *testing.T) { - query := &annotations.ItemQuery{ + query := annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 0, @@ -399,7 +399,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Can update annotation with additional tags", func(t *testing.T) { - query := &annotations.ItemQuery{ + query := annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 0, @@ -432,7 +432,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Can update annotations with data", func(t *testing.T) { - query := &annotations.ItemQuery{ + query := annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 0, @@ -468,7 +468,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Can delete annotation", func(t *testing.T) { - query := &annotations.ItemQuery{ + query := annotations.ItemQuery{ OrgID: 1, DashboardID: 1, From: 0, @@ -512,7 +512,7 @@ func TestIntegrationAnnotations(t *testing.T) { CanAccessDashAnnotations: true, } - query := &annotations.ItemQuery{ + query := annotations.ItemQuery{ OrgID: 1, AnnotationID: annotation3.ID, SignedInUser: testUser, @@ -531,7 +531,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Should find tags by key", func(t *testing.T) { - result, err := store.GetTags(context.Background(), &annotations.TagsQuery{ + result, err := store.GetTags(context.Background(), annotations.TagsQuery{ OrgID: 1, Tag: "server", }) @@ -542,7 +542,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Should find tags by value", func(t *testing.T) { - result, err := store.GetTags(context.Background(), &annotations.TagsQuery{ + result, err := store.GetTags(context.Background(), annotations.TagsQuery{ OrgID: 1, Tag: "outage", }) @@ -555,7 +555,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Should not find tags in other org", func(t *testing.T) { - result, err := store.GetTags(context.Background(), &annotations.TagsQuery{ + result, err := store.GetTags(context.Background(), annotations.TagsQuery{ OrgID: 0, Tag: "server-1", }) @@ -564,7 +564,7 @@ func TestIntegrationAnnotations(t *testing.T) { }) t.Run("Should not find tags that do not exist", func(t *testing.T) { - result, err := store.GetTags(context.Background(), &annotations.TagsQuery{ + result, err := store.GetTags(context.Background(), annotations.TagsQuery{ OrgID: 0, Tag: "unknown:tag", }) @@ -650,7 +650,7 @@ func benchmarkFindTags(b *testing.B, numAnnotations int) { b.ResetTimer() for i := 0; i < b.N; i++ { - result, err := store.GetTags(context.Background(), &annotations.TagsQuery{ + result, err := store.GetTags(context.Background(), annotations.TagsQuery{ OrgID: 1, Tag: "outage", }) diff --git a/pkg/services/apiserver/builder/helper.go b/pkg/services/apiserver/builder/helper.go index 1b7358cdd71..fd2a0e2fab9 100644 --- a/pkg/services/apiserver/builder/helper.go +++ b/pkg/services/apiserver/builder/helper.go @@ -84,6 +84,7 @@ func getDefaultBuildHandlerChainFunc(builders []APIGroupBuilder) BuildHandlerCha handler = filters.WithAcceptHeader(handler) handler = filters.WithPathRewriters(handler, PathRewriters) handler = k8stracing.WithTracing(handler, c.TracerProvider, "KubernetesAPI") + handler = filters.WithExtractJaegerTrace(handler) // Configure filters.WithPanicRecovery to not crash on panic utilruntime.ReallyCrash = false diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go index 76d0a68a1da..c2c5bc9ef92 100644 --- a/pkg/services/apiserver/standalone/factory.go +++ b/pkg/services/apiserver/standalone/factory.go @@ -1,168 +1,28 @@ package standalone import ( - "context" - "fmt" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/prometheus/client_golang/prometheus" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - genericapiserver "k8s.io/apiserver/pkg/server" - - "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/registry/apis/datasource" - "github.com/grafana/grafana/pkg/registry/apis/query" - "github.com/grafana/grafana/pkg/registry/apis/query/client" - "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/apiserver/options" - "github.com/grafana/grafana/pkg/services/featuremgmt" - testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" + "github.com/urfave/cli/v2" ) +type BuildInfo struct { + Version string + Commit string + EnterpriseCommit string + BuildBranch string + BuildStamp string +} + type APIServerFactory interface { - // Called before the groups are loaded so any custom command can be registered - GetOptions() options.OptionsProvider - - // Given the flags, what can we produce - GetEnabled(runtime []RuntimeConfig) ([]schema.GroupVersion, error) - - // Optional override for apiserver's BuildHandlerChainFunc, return nil if you want to use the grafana's default chain func defined in pkg/services/apiserver/builder/helper.go - GetBuildHandlerChainFunc(tracer tracing.Tracer, builders []builder.APIGroupBuilder) builder.BuildHandlerChainFunc - - // Make an API server for a given group+version - MakeAPIServer(ctx context.Context, tracer tracing.Tracer, gv schema.GroupVersion) (builder.APIGroupBuilder, error) - - Shutdown() + GetCLICommand(info BuildInfo) *cli.Command } -// Zero dependency provider for testing -func GetDummyAPIFactory() APIServerFactory { - return &DummyAPIFactory{} +// NOOP +func ProvideAPIServerFactory() APIServerFactory { + return &NoOpAPIServerFactory{} } -type DummyAPIFactory struct{} +type NoOpAPIServerFactory struct{} -func (p *DummyAPIFactory) GetOptions() options.OptionsProvider { +func (f *NoOpAPIServerFactory) GetCLICommand(info BuildInfo) *cli.Command { return nil } - -func (p *DummyAPIFactory) GetBuildHandlerChainFunc(_ tracing.Tracer, builders []builder.APIGroupBuilder) builder.BuildHandlerChainFunc { - return nil -} - -func (p *DummyAPIFactory) GetEnabled(runtime []RuntimeConfig) ([]schema.GroupVersion, error) { - gv := []schema.GroupVersion{} - for _, cfg := range runtime { - if !cfg.Enabled { - return nil, fmt.Errorf("only enabled supported now") - } - if cfg.Group == "all" { - return nil, fmt.Errorf("all not yet supported") - } - gv = append(gv, schema.GroupVersion{Group: cfg.Group, Version: cfg.Version}) - } - return gv, nil -} - -func (p *DummyAPIFactory) ApplyTo(config *genericapiserver.RecommendedConfig) error { - return nil -} - -func (p *DummyAPIFactory) MakeAPIServer(_ context.Context, tracer tracing.Tracer, gv schema.GroupVersion) (builder.APIGroupBuilder, error) { - if gv.Version != "v0alpha1" { - return nil, fmt.Errorf("only alpha supported now") - } - - switch gv.Group { - // Only works with testdata - case "query.grafana.app": - return query.NewQueryAPIBuilder( - featuremgmt.WithFeatures(), - &query.CommonDataSourceClientSupplier{ - Client: client.NewTestDataClient(), - }, - client.NewTestDataRegistry(), - nil, // legacy lookup - prometheus.NewRegistry(), // ??? - tracer, - ) - - case "testdata.datasource.grafana.app": - return datasource.NewDataSourceAPIBuilder( - plugins.JSONData{ - ID: "grafana-testdata-datasource", - }, - testdatasource.ProvideService(), // the client - &pluginDatasourceImpl{ - startup: v1.Now(), - }, - &pluginDatasourceImpl{}, // stub - &actest.FakeAccessControl{ExpectedEvaluate: true}, - true, // show query types - ) - } - - return nil, fmt.Errorf("unsupported group") -} - -func (p *DummyAPIFactory) Shutdown() {} - -// Simple stub for standalone datasource testing -type pluginDatasourceImpl struct { - startup v1.Time -} - -var ( - _ datasource.PluginDatasourceProvider = (*pluginDatasourceImpl)(nil) -) - -// Get implements PluginDatasourceProvider. -func (p *pluginDatasourceImpl) Get(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) { - all, err := p.List(ctx) - if err != nil { - return nil, err - } - for idx, v := range all.Items { - if v.Name == uid { - return &all.Items[idx], nil - } - } - return nil, fmt.Errorf("not found") -} - -// List implements PluginConfigProvider. -func (p *pluginDatasourceImpl) List(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - - return &v0alpha1.DataSourceConnectionList{ - TypeMeta: v0alpha1.GenericConnectionResourceInfo.TypeMeta(), - Items: []v0alpha1.DataSourceConnection{ - { - ObjectMeta: v1.ObjectMeta{ - Name: "PD8C576611E62080A", - Namespace: info.Value, // the raw namespace value - CreationTimestamp: p.startup, - }, - Title: "gdev-testdata", - }, - }, - }, nil -} - -// PluginContextForDataSource implements PluginConfigProvider. -func (*pluginDatasourceImpl) GetInstanceSettings(ctx context.Context, uid string) (*backend.DataSourceInstanceSettings, error) { - return &backend.DataSourceInstanceSettings{}, nil -} - -// PluginContextWrapper -func (*pluginDatasourceImpl) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) { - return backend.PluginContext{DataSourceInstanceSettings: datasourceSettings}, nil -} diff --git a/pkg/services/cloudmigration/api/api.go b/pkg/services/cloudmigration/api/api.go index b647433e114..1cd851a9717 100644 --- a/pkg/services/cloudmigration/api/api.go +++ b/pkg/services/cloudmigration/api/api.go @@ -392,6 +392,7 @@ func (cma *CloudMigrationAPI) GetSnapshotList(c *contextmodel.ReqContext) respon SessionUID: uid, Limit: c.QueryInt("limit"), Page: c.QueryInt("page"), + Sort: c.Query("sort"), } if q.Limit == 0 { q.Limit = 100 diff --git a/pkg/services/cloudmigration/api/api_test.go b/pkg/services/cloudmigration/api/api_test.go index 5926276a5d0..1b56c8a4c20 100644 --- a/pkg/services/cloudmigration/api/api_test.go +++ b/pkg/services/cloudmigration/api/api_test.go @@ -395,7 +395,23 @@ func TestCloudMigrationAPI_GetSnapshotList(t *testing.T) { requestUrl: "/api/cloudmigration/migration/1234/snapshots", basicRole: org.RoleAdmin, expectedHttpResult: http.StatusOK, - expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z"},{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"0001-01-01T00:00:00Z","finished":"0001-01-01T00:00:00Z"}]}`, + expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T17:30:40Z","finished":"0001-01-01T00:00:00Z"},{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T18:30:40Z","finished":"0001-01-01T00:00:00Z"}]}`, + }, + { + desc: "with limit query param should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234/snapshots?limit=1", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T17:30:40Z","finished":"0001-01-01T00:00:00Z"}]}`, + }, + { + desc: "with sort query param should return 200 if everything is ok", + requestHttpMethod: http.MethodGet, + requestUrl: "/api/cloudmigration/migration/1234/snapshots?sort=latest", + basicRole: org.RoleAdmin, + expectedHttpResult: http.StatusOK, + expectedBody: `{"snapshots":[{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T18:30:40Z","finished":"0001-01-01T00:00:00Z"},{"uid":"fake_uid","status":"CREATING","sessionUid":"1234","created":"2024-06-05T17:30:40Z","finished":"0001-01-01T00:00:00Z"}]}`, }, { desc: "should return 403 if no used is not admin", diff --git a/pkg/services/cloudmigration/api/dtos.go b/pkg/services/cloudmigration/api/dtos.go index f301dea4de7..d16f616739e 100644 --- a/pkg/services/cloudmigration/api/dtos.go +++ b/pkg/services/cloudmigration/api/dtos.go @@ -313,6 +313,11 @@ type GetSnapshotListParams struct { // Session UID of a session // in: path UID string `json:"uid"` + + // Sort with value latest to return results sorted in descending order. + // in:query + // required:false + Sort string `json:"sort"` } // swagger:response snapshotListResponse diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index f78a32dad8c..886cb8565ef 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/gcom" + "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/secrets" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" @@ -53,13 +54,14 @@ type Service struct { gmsClient gmsclient.Client objectStorage objectstorage.ObjectStorage - dsService datasources.DataSourceService - gcomService gcom.Service - dashboardService dashboards.DashboardService - folderService folder.Service - pluginStore pluginstore.Store - secretsService secrets.Service - kvStore *kvstore.NamespacedKVStore + dsService datasources.DataSourceService + gcomService gcom.Service + dashboardService dashboards.DashboardService + folderService folder.Service + pluginStore pluginstore.Store + secretsService secrets.Service + kvStore *kvstore.NamespacedKVStore + libraryElementsService libraryelements.Service api *api.CloudMigrationAPI tracer tracing.Tracer @@ -93,24 +95,26 @@ func ProvideService( folderService folder.Service, pluginStore pluginstore.Store, kvStore kvstore.KVStore, + libraryElementsService libraryelements.Service, ) (cloudmigration.Service, error) { if !features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) { return &NoopServiceImpl{}, nil } s := &Service{ - store: &sqlStore{db: db, secretsStore: secretsStore, secretsService: secretsService}, - log: log.New(LogPrefix), - cfg: cfg, - features: features, - dsService: dsService, - tracer: tracer, - metrics: newMetrics(), - secretsService: secretsService, - dashboardService: dashboardService, - folderService: folderService, - pluginStore: pluginStore, - kvStore: kvstore.WithNamespace(kvStore, 0, "cloudmigration"), + store: &sqlStore{db: db, secretsStore: secretsStore, secretsService: secretsService}, + log: log.New(LogPrefix), + cfg: cfg, + features: features, + dsService: dsService, + tracer: tracer, + metrics: newMetrics(), + secretsService: secretsService, + dashboardService: dashboardService, + folderService: folderService, + pluginStore: pluginStore, + kvStore: kvstore.WithNamespace(kvStore, 0, "cloudmigration"), + libraryElementsService: libraryElementsService, } s.api = api.RegisterApi(routeRegister, s, tracer) @@ -179,7 +183,8 @@ func (s *Service) GetToken(ctx context.Context) (gcom.TokenView, error) { } logger.Info("cloud migration token not found") - return gcom.TokenView{}, cloudmigration.ErrTokenNotFound + return gcom.TokenView{}, fmt.Errorf("fetching cloud migration token: instance=%+v accessPolicyName=%s accessTokenName=%s %w", + instance, accessPolicyName, accessTokenName, cloudmigration.ErrTokenNotFound) } func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessTokenResponse, error) { @@ -300,7 +305,7 @@ func (s *Service) ValidateToken(ctx context.Context, cm cloudmigration.CloudMigr defer span.End() if err := s.gmsClient.ValidateKey(ctx, cm); err != nil { - return fmt.Errorf("validating key: %w", err) + return fmt.Errorf("validating token: %w", err) } return nil diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go index 95e8a28fbad..ff068b68a0c 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_test.go @@ -24,6 +24,8 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" + libraryelementsfake "github.com/grafana/grafana/pkg/services/libraryelements/fake" + libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" secretsfakes "github.com/grafana/grafana/pkg/services/secrets/fakes" secretskv "github.com/grafana/grafana/pkg/services/secrets/kvstore" @@ -58,7 +60,7 @@ func Test_CreateGetAndDeleteToken(t *testing.T) { assert.NoError(t, err) _, err = s.GetToken(context.Background()) - assert.ErrorIs(t, cloudmigration.ErrTokenNotFound, err) + assert.ErrorIs(t, err, cloudmigration.ErrTokenNotFound) cm := cloudmigration.CloudMigrationSession{} err = s.ValidateToken(context.Background(), cm) @@ -562,6 +564,36 @@ func TestReportEvent(t *testing.T) { }) } +func TestGetLibraryElementsCommands(t *testing.T) { + s := setUpServiceTest(t, false).(*Service) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + libraryElementService, ok := s.libraryElementsService.(*libraryelementsfake.LibraryElementService) + require.True(t, ok) + require.NotNil(t, libraryElementService) + + folderUID := "folder-uid" + createLibraryElementCmd := libraryelements.CreateLibraryElementCommand{ + FolderUID: &folderUID, + Name: "library-element-1", + Model: []byte{}, + Kind: int64(libraryelements.PanelElement), + UID: "library-element-uid-1", + } + + user := &user.SignedInUser{OrgID: 1} + + _, err := libraryElementService.CreateElement(ctx, user, createLibraryElementCmd) + require.NoError(t, err) + + cmds, err := s.getLibraryElementsCommands(ctx, user) + require.NoError(t, err) + require.Len(t, cmds, 1) + require.Equal(t, createLibraryElementCmd.UID, cmds[0].UID) +} + func ctxWithSignedInUser() context.Context { c := &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{OrgID: 1}, @@ -617,7 +649,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi featuremgmt.FlagDashboardRestore), sqlStore, dsService, - secretskv.NewFakeSQLSecretsKVStore(t), + secretskv.NewFakeSQLSecretsKVStore(t, sqlStore), secretsService, rr, prometheus.DefaultRegisterer, @@ -626,6 +658,7 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool) cloudmigration.Servi mockFolder, &pluginstore.FakePluginStore{}, kvstore.ProvideService(sqlStore), + &libraryelementsfake.LibraryElementService{}, ) require.NoError(t, err) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go index 250c837b14e..50ccfff8ef9 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go @@ -3,6 +3,7 @@ package fake import ( "context" "fmt" + "sort" "time" "github.com/grafana/grafana/pkg/services/cloudmigration" @@ -108,18 +109,31 @@ func (m FakeServiceImpl) GetSnapshotList(ctx context.Context, query cloudmigrati if m.ReturnError { return nil, fmt.Errorf("mock error") } - return []cloudmigration.CloudMigrationSnapshot{ + + cloudSnapshots := []cloudmigration.CloudMigrationSnapshot{ { UID: "fake_uid", SessionUID: query.SessionUID, Status: cloudmigration.SnapshotStatusCreating, + Created: time.Date(2024, 6, 5, 17, 30, 40, 0, time.UTC), }, { UID: "fake_uid", SessionUID: query.SessionUID, Status: cloudmigration.SnapshotStatusCreating, + Created: time.Date(2024, 6, 5, 18, 30, 40, 0, time.UTC), }, - }, nil + } + + if query.Sort == "latest" { + sort.Slice(cloudSnapshots, func(first, second int) bool { + return cloudSnapshots[first].Created.After(cloudSnapshots[second].Created) + }) + } + if query.Limit > 0 { + return cloudSnapshots[0:min(len(cloudSnapshots), query.Limit)], nil + } + return cloudSnapshots, nil } func (m FakeServiceImpl) UploadSnapshot(ctx context.Context, sessionUid string, snapshotUid string) error { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go index b2841f9d9f0..ece57dc92ed 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/snapshot_mgmt.go @@ -3,6 +3,7 @@ package cloudmigrationimpl import ( "context" cryptoRand "crypto/rand" + "encoding/json" "fmt" "os" "path/filepath" @@ -18,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" + libraryelements "github.com/grafana/grafana/pkg/services/libraryelements/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/retryer" "golang.org/x/crypto/nacl/box" @@ -38,9 +40,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S return nil, err } + libraryElements, err := s.getLibraryElementsCommands(ctx, signedInUser) + if err != nil { + s.log.Error("Failed to get library elements", "err", err) + return nil, err + } + migrationDataSlice := make( []cloudmigration.MigrateDataRequestItem, 0, - len(dataSources)+len(dashs)+len(folders), + len(dataSources)+len(dashs)+len(folders)+len(libraryElements), ) for _, ds := range dataSources { @@ -78,6 +86,15 @@ func (s *Service) getMigrationDataJSON(ctx context.Context, signedInUser *user.S }) } + for _, libraryElement := range libraryElements { + migrationDataSlice = append(migrationDataSlice, cloudmigration.MigrateDataRequestItem{ + Type: cloudmigration.LibraryElementDataType, + RefID: libraryElement.UID, + Name: libraryElement.Name, + Data: libraryElement, + }) + } + migrationData := &cloudmigration.MigrateDataRequest{ Items: migrationDataSlice, } @@ -169,6 +186,60 @@ func (s *Service) getDashboardAndFolderCommands(ctx context.Context, signedInUse return dashboardCmds, folderCmds, nil } +type libraryElement struct { + FolderUID *string `json:"folderUid"` + Name string `json:"name"` + UID string `json:"uid"` + Model json.RawMessage `json:"model"` + Kind int64 `json:"kind"` +} + +// getLibraryElementsCommands returns the json payloads required by the library elements creation API +func (s *Service) getLibraryElementsCommands(ctx context.Context, signedInUser *user.SignedInUser) ([]libraryElement, error) { + const perPage = 100 + + cmds := make([]libraryElement, 0) + + page := 1 + count := 0 + + for { + query := libraryelements.SearchLibraryElementsQuery{ + PerPage: perPage, + Page: page, + } + + libraryElements, err := s.libraryElementsService.GetAllElements(ctx, signedInUser, query) + if err != nil { + return nil, fmt.Errorf("failed to get all library elements: %w", err) + } + + for _, element := range libraryElements.Elements { + var folderUID *string + if len(element.FolderUID) > 0 { + folderUID = &element.FolderUID + } + + cmds = append(cmds, libraryElement{ + FolderUID: folderUID, + Name: element.Name, + Model: element.Model, + Kind: element.Kind, + UID: element.UID, + }) + } + + page += 1 + count += libraryElements.PerPage + + if len(libraryElements.Elements) == 0 || count >= int(libraryElements.TotalCount) { + break + } + } + + return cmds, nil +} + // asynchronous process for writing the snapshot to the filesystem and updating the snapshot status func (s *Service) buildSnapshot(ctx context.Context, signedInUser *user.SignedInUser, maxItemsPerPartition uint32, metadata []byte, snapshotMeta cloudmigration.CloudMigrationSnapshot) error { // TODO -- make sure we can only build one snapshot at a time @@ -229,6 +300,7 @@ func (s *Service) buildSnapshot(ctx context.Context, signedInUser *user.SignedIn for _, resourceType := range []cloudmigration.MigrateDataType{ cloudmigration.DatasourceDataType, cloudmigration.FolderDataType, + cloudmigration.LibraryElementDataType, cloudmigration.DashboardDataType, } { for chunk := range slices.Chunk(resourcesGroupedByType[resourceType], int(maxItemsPerPartition)) { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go index 86dea62f7d2..abccfbaf653 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store.go @@ -23,9 +23,10 @@ type sqlStore struct { } const ( - tableName = "cloud_migration_resource" - secretType = "cloudmigration-snapshot-encryption-key" - GetAllSnapshots = -1 + tableName = "cloud_migration_resource" + secretType = "cloudmigration-snapshot-encryption-key" + GetAllSnapshots = -1 + GetSnapshotListSortingLatest = "latest" ) func (ss *sqlStore) GetMigrationSessionByUID(ctx context.Context, uid string) (*cloudmigration.CloudMigrationSession, error) { @@ -278,7 +279,9 @@ func (ss *sqlStore) GetSnapshotList(ctx context.Context, query cloudmigration.Li offset := (query.Page - 1) * query.Limit sess.Limit(query.Limit, offset) } - sess.OrderBy("created DESC") + if query.Sort == GetSnapshotListSortingLatest { + sess.OrderBy("cloud_migration_snapshot.created DESC") + } return sess.Find(&snapshots, &cloudmigration.CloudMigrationSnapshot{ SessionUID: query.SessionUID, }) @@ -432,16 +435,16 @@ func (ss *sqlStore) encryptToken(ctx context.Context, cm *cloudmigration.CloudMi func (ss *sqlStore) decryptToken(ctx context.Context, cm *cloudmigration.CloudMigrationSession) error { if cm == nil { - return cloudmigration.ErrMigrationNotFound + return fmt.Errorf("unable to decypt token because migration session was not found: %w", cloudmigration.ErrMigrationNotFound) } if len(cm.AuthToken) == 0 { - return cloudmigration.ErrTokenNotFound + return fmt.Errorf("unable to decrypt token because token is empty: %w", cloudmigration.ErrTokenNotFound) } decoded, err := base64.StdEncoding.DecodeString(cm.AuthToken) if err != nil { - return fmt.Errorf("token could not be decoded") + return fmt.Errorf("unable to base64 decode token: %w", err) } t, err := ss.secretsService.Decrypt(ctx, decoded) diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go index 1b9808405ea..58b98f6c1ad 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/xorm_store_test.go @@ -3,7 +3,6 @@ package cloudmigrationimpl import ( "context" "encoding/base64" - "slices" "strconv" "testing" @@ -241,9 +240,46 @@ func TestGetSnapshotList(t *testing.T) { for _, snapshot := range snapshots { ids = append(ids, snapshot.UID) } - slices.Sort(ids) // There are 3 snapshots in the db but only 2 of them belong to this specific session. + assert.Equal(t, []string{"poiuy", "lkjhg"}, ids) + }) + + t.Run("returns only one snapshot that belongs to a session", func(t *testing.T) { + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 1}) + require.NoError(t, err) + assert.Len(t, snapshots, 1) + }) + + t.Run("return no snapshots if limit is set to 0", func(t *testing.T) { + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 0}) + require.NoError(t, err) + assert.Empty(t, snapshots) + }) + + t.Run("returns paginated snapshot that belongs to a session", func(t *testing.T) { + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 2, Limit: 1}) + require.NoError(t, err) + + ids := make([]string, 0) + for _, snapshot := range snapshots { + ids = append(ids, snapshot.UID) + } + + // Return paginated snapshot of the 2 belonging to this specific session + assert.Equal(t, []string{"lkjhg"}, ids) + }) + + t.Run("returns desc sorted list of snapshots that belong to a session", func(t *testing.T) { + snapshots, err := s.GetSnapshotList(ctx, cloudmigration.ListSnapshotsQuery{SessionUID: sessionUID, Page: 1, Limit: 100, Sort: "latest"}) + require.NoError(t, err) + + ids := make([]string, 0) + for _, snapshot := range snapshots { + ids = append(ids, snapshot.UID) + } + + // Return desc sorted snapshots belonging to this specific session assert.Equal(t, []string{"lkjhg", "poiuy"}, ids) }) @@ -320,7 +356,7 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { s := &sqlStore{ db: testDB, secretsService: fakeSecrets.FakeSecretsService{}, - secretsStore: secretskv.NewFakeSQLSecretsKVStore(t), + secretsStore: secretskv.NewFakeSQLSecretsKVStore(t, testDB), } ctx := context.Background() @@ -345,7 +381,7 @@ func setUpTest(t *testing.T) (*sqlstore.SQLStore, *sqlStore) { cloud_migration_snapshot (session_uid, uid, created, updated, finished, status) VALUES ('qwerty', 'poiuy', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), - ('qwerty', 'lkjhg', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), + ('qwerty', 'lkjhg', '2024-03-26 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"), ('zxcvbn', 'mnbvvc', '2024-03-25 15:30:36.000', '2024-03-27 15:30:43.000', '2024-03-27 15:30:43.000', "finished"); `, ) diff --git a/pkg/services/cloudmigration/gmsclient/client.go b/pkg/services/cloudmigration/gmsclient/client.go index d15b03a9e95..91a7f9c4eab 100644 --- a/pkg/services/cloudmigration/gmsclient/client.go +++ b/pkg/services/cloudmigration/gmsclient/client.go @@ -8,7 +8,6 @@ import ( type Client interface { ValidateKey(context.Context, cloudmigration.CloudMigrationSession) error - MigrateData(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.MigrateDataRequest) (*cloudmigration.MigrateDataResponse, error) StartSnapshot(context.Context, cloudmigration.CloudMigrationSession) (*cloudmigration.StartSnapshotResponse, error) GetSnapshotStatus(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot, int) (*cloudmigration.GetSnapshotStatusResponse, error) CreatePresignedUploadUrl(context.Context, cloudmigration.CloudMigrationSession, cloudmigration.CloudMigrationSnapshot) (string, error) diff --git a/pkg/services/cloudmigration/gmsclient/gms_client.go b/pkg/services/cloudmigration/gmsclient/gms_client.go index d9804d4e157..9cbe60d97e3 100644 --- a/pkg/services/cloudmigration/gmsclient/gms_client.go +++ b/pkg/services/cloudmigration/gmsclient/gms_client.go @@ -71,52 +71,6 @@ func (c *gmsClientImpl) ValidateKey(ctx context.Context, cm cloudmigration.Cloud return nil } -// Deprecated -func (c *gmsClientImpl) MigrateData(ctx context.Context, cm cloudmigration.CloudMigrationSession, request cloudmigration.MigrateDataRequest) (result *cloudmigration.MigrateDataResponse, err error) { - path := fmt.Sprintf("%s/api/v1/migrate-data", c.buildBasePath(cm.ClusterSlug)) - - reqDTO := convertRequestToDTO(request) - body, err := json.Marshal(reqDTO) - if err != nil { - return nil, fmt.Errorf("error marshaling request: %w", err) - } - - // Send the request to GMS with the associated auth token - req, err := http.NewRequest(http.MethodPost, path, bytes.NewReader(body)) - if err != nil { - c.log.Error("error creating http request for cloud migration run", "err", err.Error()) - return nil, fmt.Errorf("http request error: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %d:%s", cm.StackID, cm.AuthToken)) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - c.log.Error("error sending http request for cloud migration run", "err", err.Error()) - return nil, fmt.Errorf("http request error: %w", err) - } - defer func() { - if closeErr := resp.Body.Close(); closeErr != nil { - err = errors.Join(err, fmt.Errorf("closing response body: %w", closeErr)) - } - }() - - if resp.StatusCode >= 400 { - c.log.Error("received error response for cloud migration run", "statusCode", resp.StatusCode) - return nil, fmt.Errorf("http request error: %w", err) - } - - var respDTO MigrateDataResponseDTO - if err := json.NewDecoder(resp.Body).Decode(&respDTO); err != nil { - c.log.Error("unmarshalling response body", "err", err.Error()) - return nil, fmt.Errorf("unmarshalling migration run response: %w", err) - } - - res := convertResponseFromDTO(respDTO) - return &res, nil -} - func (c *gmsClientImpl) StartSnapshot(ctx context.Context, session cloudmigration.CloudMigrationSession) (out *cloudmigration.StartSnapshotResponse, err error) { path := fmt.Sprintf("%s/api/v1/start-snapshot", c.buildBasePath(session.ClusterSlug)) @@ -297,42 +251,8 @@ func (c *gmsClientImpl) ReportEvent(ctx context.Context, session cloudmigration. func (c *gmsClientImpl) buildBasePath(clusterSlug string) string { domain := c.cfg.CloudMigration.GMSDomain - if strings.HasPrefix(domain, "http://localhost") { + if strings.HasPrefix(domain, "http://") || strings.HasPrefix(domain, "https://") { return domain } return fmt.Sprintf("https://cms-%s.%s/cloud-migrations", clusterSlug, domain) } - -func convertRequestToDTO(request cloudmigration.MigrateDataRequest) MigrateDataRequestDTO { - items := make([]MigrateDataRequestItemDTO, len(request.Items)) - for i := 0; i < len(request.Items); i++ { - item := request.Items[i] - items[i] = MigrateDataRequestItemDTO{ - Type: MigrateDataType(item.Type), - RefID: item.RefID, - Name: item.Name, - Data: item.Data, - } - } - r := MigrateDataRequestDTO{ - Items: items, - } - return r -} - -func convertResponseFromDTO(result MigrateDataResponseDTO) cloudmigration.MigrateDataResponse { - items := make([]cloudmigration.CloudMigrationResource, len(result.Items)) - for i := 0; i < len(result.Items); i++ { - item := result.Items[i] - items[i] = cloudmigration.CloudMigrationResource{ - Type: cloudmigration.MigrateDataType(item.Type), - RefID: item.RefID, - Status: cloudmigration.ItemStatus(item.Status), - Error: item.Error, - } - } - return cloudmigration.MigrateDataResponse{ - RunUID: result.RunUID, - Items: items, - } -} diff --git a/pkg/services/cloudmigration/gmsclient/gms_client_test.go b/pkg/services/cloudmigration/gmsclient/gms_client_test.go index 0e04bb0ad75..a2eb92d76cb 100644 --- a/pkg/services/cloudmigration/gmsclient/gms_client_test.go +++ b/pkg/services/cloudmigration/gmsclient/gms_client_test.go @@ -35,13 +35,19 @@ func Test_buildBasePath(t *testing.T) { expected string }{ { - description: "domain starts with http://localhost, should return domain", - domain: "http://localhost:8080", + description: "domain starts with http://, should return domain", + domain: "http://some-domain:8080", clusterSlug: "anything", - expected: "http://localhost:8080", + expected: "http://some-domain:8080", }, { - description: "domain doesn't start with http://localhost, should build a string using the domain and clusterSlug", + description: "domain starts with https://, should return domain", + domain: "https://some-domain:8080", + clusterSlug: "anything", + expected: "https://some-domain:8080", + }, + { + description: "domain doesn't start with http or https, should build a string using the domain and clusterSlug", domain: "gms-dev", clusterSlug: "us-east-1", expected: "https://cms-us-east-1.gms-dev/cloud-migrations", diff --git a/pkg/services/cloudmigration/model.go b/pkg/services/cloudmigration/model.go index 0eb8a8d04d6..786df738ca5 100644 --- a/pkg/services/cloudmigration/model.go +++ b/pkg/services/cloudmigration/model.go @@ -144,6 +144,7 @@ type ListSnapshotsQuery struct { SessionUID string Page int Limit int + Sort string } type UpdateSnapshotCmd struct { diff --git a/pkg/services/dashboards/accesscontrol.go b/pkg/services/dashboards/accesscontrol.go index c4b53df4c87..57dafdd454c 100644 --- a/pkg/services/dashboards/accesscontrol.go +++ b/pkg/services/dashboards/accesscontrol.go @@ -43,7 +43,7 @@ var ( ) // NewFolderNameScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:name:" into an uid based scope. -func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeFoldersProvider.GetResourceScopeName("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewFolderNameScopeResolver") @@ -63,7 +63,7 @@ func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Se return nil, err } - result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderStore) if err != nil { return nil, err } @@ -74,7 +74,7 @@ func NewFolderNameScopeResolver(folderDB folder.FolderStore, folderSvc folder.Se } // NewFolderIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:id:" into an uid based scope. -func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeFoldersProvider.GetResourceScope("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewFolderIDScopeResolver") @@ -98,7 +98,7 @@ func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Serv return nil, err } - result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderSvc) + result, err := GetInheritedScopes(ctx, folder.OrgID, folder.UID, folderStore) if err != nil { return nil, err } @@ -110,7 +110,7 @@ func NewFolderIDScopeResolver(folderDB folder.FolderStore, folderSvc folder.Serv // NewFolderUIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "folders:uid:" // into uid based scopes for folder and its parents -func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewFolderUIDScopeResolver(folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeFoldersProvider.GetResourceScopeUID("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewFolderUIDScopeResolver") @@ -125,7 +125,7 @@ func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttrib return nil, err } - inheritedScopes, err := GetInheritedScopes(ctx, orgID, uid, folderSvc) + inheritedScopes, err := GetInheritedScopes(ctx, orgID, uid, folderStore) if err != nil { return nil, err } @@ -135,7 +135,7 @@ func NewFolderUIDScopeResolver(folderSvc folder.Service) (string, ac.ScopeAttrib // NewDashboardIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:id:" // into uid based scopes for both dashboard and folder -func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeDashboardsProvider.GetResourceScope("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewDashboardIDScopeResolver") @@ -155,13 +155,13 @@ func NewDashboardIDScopeResolver(folderDB folder.FolderStore, ds DashboardServic return nil, err } - return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderSvc) + return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderStore) }) } // NewDashboardUIDScopeResolver provides an ScopeAttributeResolver that is able to convert a scope prefixed with "dashboards:uid:" // into uid based scopes for both dashboard and folder -func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardService, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeDashboardsProvider.GetResourceScopeUID("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.NewDashboardUIDScopeResolver") @@ -181,11 +181,11 @@ func NewDashboardUIDScopeResolver(folderDB folder.FolderStore, ds DashboardServi return nil, err } - return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderSvc) + return resolveDashboardScope(ctx, folderDB, orgID, dashboard, folderStore) }) } -func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, orgID int64, dashboard *Dashboard, folderSvc folder.Service) ([]string, error) { +func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, orgID int64, dashboard *Dashboard, folderStore folder.Store) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.resolveDashboardScope") span.End() @@ -208,7 +208,7 @@ func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, org folderUID = folder.UID } - result, err := GetInheritedScopes(ctx, orgID, folderUID, folderSvc) + result, err := GetInheritedScopes(ctx, orgID, folderUID, folderStore) if err != nil { return nil, err } @@ -223,17 +223,24 @@ func resolveDashboardScope(ctx context.Context, folderDB folder.FolderStore, org return result, nil } -func GetInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderSvc folder.Service) ([]string, error) { +func GetInheritedScopes(ctx context.Context, orgID int64, folderUID string, folderStore folder.Store) ([]string, error) { ctx, span := tracer.Start(ctx, "dashboards.GetInheritedScopes") span.End() if folderUID == ac.GeneralFolderUID { return nil, nil } - ancestors, err := folderSvc.GetParents(ctx, folder.GetParentsQuery{ - UID: folderUID, - OrgID: orgID, - }) + + var ancestors []*folder.Folder + var err error + if folderUID == folder.SharedWithMeFolderUID { + ancestors = []*folder.Folder{&folder.SharedWithMeFolder} + } else { + ancestors, err = folderStore.GetParents(ctx, folder.GetParentsQuery{ + UID: folderUID, + OrgID: orgID, + }) + } if err != nil { if errors.Is(err, folder.ErrFolderNotFound) { diff --git a/pkg/services/dashboards/accesscontrol_test.go b/pkg/services/dashboards/accesscontrol_test.go index 57e1d8e80e0..ead4055ac3f 100644 --- a/pkg/services/dashboards/accesscontrol_test.go +++ b/pkg/services/dashboards/accesscontrol_test.go @@ -18,7 +18,7 @@ import ( func TestNewFolderNameScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + prefix, _ := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) require.Equal(t, "folders:name:", prefix) }) @@ -31,7 +31,7 @@ func TestNewFolderNameScopeResolver(t *testing.T) { scope := "folders:name:" + title - _, resolver := NewFolderNameScopeResolver(folderStore, foldertest.NewFakeService()) + _, resolver := NewFolderNameScopeResolver(folderStore, folder.NewFakeStore()) resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope) require.NoError(t, err) require.Len(t, resolvedScopes, 1) @@ -49,8 +49,8 @@ func TestNewFolderNameScopeResolver(t *testing.T) { scope := "folders:name:" + title - folderSvc := foldertest.NewFakeService() - folderSvc.ExpectedFolders = []*folder.Folder{ + fStore := folder.NewFakeStore() + fStore.ExpectedParentFolders = []*folder.Folder{ { UID: "parent", }, @@ -58,7 +58,7 @@ func TestNewFolderNameScopeResolver(t *testing.T) { UID: "grandparent", }, } - _, resolver := NewFolderNameScopeResolver(folderStore, folderSvc) + _, resolver := NewFolderNameScopeResolver(folderStore, fStore) resolvedScopes, err := resolver.Resolve(context.Background(), orgId, scope) require.NoError(t, err) @@ -75,20 +75,20 @@ func TestNewFolderNameScopeResolver(t *testing.T) { folderStore.AssertCalled(t, "GetFolderByTitle", mock.Anything, orgId, title, mock.Anything) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) { - _, resolver := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver := NewFolderNameScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:name:") require.ErrorIs(t, err, ac.ErrInvalidScope) }) t.Run("returns 'not found' if folder does not exist", func(t *testing.T) { folderStore := foldertest.NewFakeFolderStore(t) - _, resolver := NewFolderNameScopeResolver(folderStore, foldertest.NewFakeService()) + _, resolver := NewFolderNameScopeResolver(folderStore, folder.NewFakeStore()) orgId := rand.Int63() folderStore.On("GetFolderByTitle", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrDashboardNotFound).Once() @@ -103,12 +103,12 @@ func TestNewFolderNameScopeResolver(t *testing.T) { func TestNewFolderIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + prefix, _ := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) require.Equal(t, "folders:id:", prefix) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:uid:123") require.ErrorIs(t, err, ac.ErrInvalidScope) @@ -118,7 +118,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { var ( orgId = rand.Int63() scope = "folders:id:0" - _, resolver = NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver = NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) ) resolved, err := resolver.Resolve(context.Background(), orgId, scope) @@ -129,7 +129,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { }) t.Run("resolver should fail if resource of input scope is empty", func(t *testing.T) { - _, resolver := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), foldertest.NewFakeService()) + _, resolver := NewFolderIDScopeResolver(foldertest.NewFakeFolderStore(t), folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "folders:id:") require.ErrorIs(t, err, ac.ErrInvalidScope) @@ -137,7 +137,7 @@ func TestNewFolderIDScopeResolver(t *testing.T) { t.Run("returns 'not found' if folder does not exist", func(t *testing.T) { folderStore := foldertest.NewFakeFolderStore(t) folderStore.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(nil, ErrDashboardNotFound).Once() - _, resolver := NewFolderIDScopeResolver(folderStore, foldertest.NewFakeService()) + _, resolver := NewFolderIDScopeResolver(folderStore, folder.NewFakeStore()) orgId := rand.Int63() scope := "folders:id:10" @@ -149,12 +149,12 @@ func TestNewFolderIDScopeResolver(t *testing.T) { func TestNewDashboardIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + prefix, _ := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, folder.NewFakeStore()) require.Equal(t, "dashboards:id:", prefix) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + _, resolver := NewDashboardIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:uid:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) @@ -162,12 +162,12 @@ func TestNewDashboardIDScopeResolver(t *testing.T) { func TestNewDashboardUIDScopeResolver(t *testing.T) { t.Run("prefix should be expected", func(t *testing.T) { - prefix, _ := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + prefix, _ := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, folder.NewFakeStore()) require.Equal(t, "dashboards:uid:", prefix) }) t.Run("resolver should fail if input scope is not expected", func(t *testing.T) { - _, resolver := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, foldertest.NewFakeService()) + _, resolver := NewDashboardUIDScopeResolver(foldertest.NewFakeFolderStore(t), &FakeDashboardService{}, folder.NewFakeStore()) _, err := resolver.Resolve(context.Background(), rand.Int63(), "dashboards:id:123") require.ErrorIs(t, err, ac.ErrInvalidScope) }) diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index f654bc428e1..b792facc2c4 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -303,7 +303,10 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) { guardian.New = origNewGuardian }) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + folderPermissions := mock.NewMockedPermissionsService() + folderStore := folderimpl.ProvideStore(sqlStore) + folderSvc := folderimpl.ProvideService(folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), + sqlStore, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) parentUID := "" for i := 0; ; i++ { diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 83c118d6047..83259785a69 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -16,6 +16,8 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -829,8 +831,11 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) { insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false) ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()) + folderPermissions := mock.NewMockedPermissionsService() folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + fStore := folderimpl.ProvideStore(sqlStore) + folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, + folderStore, sqlStore, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) user := &user.SignedInUser{ OrgID: 1, @@ -936,8 +941,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { t.Skip("skipping integration test") } - sqlStore := db.InitTestDB(t) - cfg := setting.NewCfg() + sqlStore, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagPanelTitleSearch) dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore), quotaService) @@ -948,7 +952,13 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) { ac := acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderServiceWithFlagOn := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + fStore := folderimpl.ProvideStore(sqlStore) + + folderPermissions, err := testutil.ProvideFolderPermissions(features, cfg, sqlStore) + require.NoError(t, err) + + folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, + folderStore, sqlStore, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) user := &user.SignedInUser{ OrgID: 1, diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 34c400461c4..90653059b10 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -66,7 +66,7 @@ func ProvideDashboardServiceImpl( cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl, - folderSvc folder.Service, r prometheus.Registerer, + folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, ) (*DashboardServiceImpl, error) { dashSvc := &DashboardServiceImpl{ cfg: cfg, @@ -81,8 +81,8 @@ func ProvideDashboardServiceImpl( metrics: newDashboardsMetrics(r), } - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, folderSvc)) - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, dashSvc, folderSvc)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderStore, dashSvc, fStore)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, dashSvc, fStore)) if err := folderSvc.RegisterService(dashSvc); err != nil { return nil, err diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index c1fb4f426a0..775ae923aef 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" "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/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" @@ -874,6 +875,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc dashboardPermissions, ac, foldertest.NewFakeService(), + folder.NewFakeStore(), nil, ) require.NoError(t, err) @@ -939,6 +941,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt dashboardPermissions, actest.FakeAccessControl{}, foldertest.NewFakeService(), + folder.NewFakeStore(), nil, ) require.NoError(t, err) @@ -963,6 +966,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto accesscontrolmock.NewMockedPermissionsService(), actest.FakeAccessControl{}, foldertest.NewFakeService(), + folder.NewFakeStore(), nil, ) require.NoError(t, err) @@ -1006,6 +1010,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string dashboardPermissions, actest.FakeAccessControl{}, foldertest.NewFakeService(), + folder.NewFakeStore(), nil, ) require.NoError(t, err) @@ -1056,6 +1061,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da accesscontrolmock.NewMockedPermissionsService(), actest.FakeAccessControl{}, foldertest.NewFakeService(), + folder.NewFakeStore(), nil, ) require.NoError(t, err) diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index c2cdf73efad..c8fad1ad7d6 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboardsnapshots" dashsnapdb "github.com/grafana/grafana/pkg/services/dashboardsnapshots/database" "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/folder/foldertest" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -98,7 +99,7 @@ func TestValidateDashboardExists(t *testing.T) { secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) - dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), nil, nil, nil, acmock.New(), foldertest.NewFakeService(), nil) + dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), nil, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil) require.NoError(t, err) s := ProvideService(dsStore, secretsService, dashSvc) ctx := context.Background() diff --git a/pkg/services/datasources/accesscontrol.go b/pkg/services/datasources/accesscontrol.go index 03d88e8e3bd..f6e5b9c8a0c 100644 --- a/pkg/services/datasources/accesscontrol.go +++ b/pkg/services/datasources/accesscontrol.go @@ -25,7 +25,6 @@ var ( var ( // ConfigurationPageAccess is used to protect the "Configure > Data sources" tab access ConfigurationPageAccess = accesscontrol.EvalAny( - accesscontrol.EvalPermission(accesscontrol.ActionDatasourcesExplore), accesscontrol.EvalPermission(ActionCreate), accesscontrol.EvalAll( accesscontrol.EvalPermission(ActionRead), diff --git a/pkg/services/extsvcauth/registry/service.go b/pkg/services/extsvcauth/registry/service.go index cd6c603bcd8..9990ce15b87 100644 --- a/pkg/services/extsvcauth/registry/service.go +++ b/pkg/services/extsvcauth/registry/service.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/serviceaccounts/extsvcaccounts" + "github.com/grafana/grafana/pkg/setting" ) var _ extsvcauth.ExternalServiceRegistry = &Registry{} @@ -28,9 +29,9 @@ type serverLocker interface { } type Registry struct { - features featuremgmt.FeatureToggles - logger log.Logger - saReg extsvcauth.ExternalServiceRegistry + enabled bool + logger log.Logger + saReg extsvcauth.ExternalServiceRegistry // FIXME (gamab): we can remove this field and use the saReg.GetExternalServiceNames directly extSvcProviders map[string]extsvcauth.AuthProvider @@ -38,10 +39,11 @@ type Registry struct { serverLock serverLocker } -func ProvideExtSvcRegistry(saSvc *extsvcaccounts.ExtSvcAccountsService, serverLock *serverlock.ServerLockService, features featuremgmt.FeatureToggles) *Registry { +func ProvideExtSvcRegistry(cfg *setting.Cfg, saSvc *extsvcaccounts.ExtSvcAccountsService, serverLock *serverlock.ServerLockService, features featuremgmt.FeatureToggles) *Registry { + enabled := features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) && cfg.ManagedServiceAccountsEnabled return &Registry{ extSvcProviders: map[string]extsvcauth.AuthProvider{}, - features: features, + enabled: enabled, lock: sync.Mutex{}, logger: log.New("extsvcauth.registry"), saReg: saSvc, @@ -110,8 +112,12 @@ func (r *Registry) RemoveExternalService(ctx context.Context, name string) error switch provider { case extsvcauth.ServiceAccounts: - if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { - r.logger.Debug("Skipping External Service removal, flag disabled", "service", name, "flag", featuremgmt.FlagExternalServiceAccounts) + if !r.enabled { + r.logger.Warn("Skipping External Service authentication, flag or configuration option is disabled", + "service", name, + "flag", featuremgmt.FlagExternalServiceAccounts, + "option", "ManagedServiceAccountsEnabled", + ) return nil } r.logger.Debug("Routing External Service removal to the External Service Account service", "service", name) @@ -140,8 +146,12 @@ func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.Exte switch cmd.AuthProvider { case extsvcauth.ServiceAccounts: - if !r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { - ctxLogger.Warn("Skipping External Service authentication, flag disabled", "service", cmd.Name, "flag", featuremgmt.FlagExternalServiceAccounts) + if !r.enabled { + ctxLogger.Warn("Skipping External Service authentication, flag or configuration option disabled", + "service", cmd.Name, + "flag", featuremgmt.FlagExternalServiceAccounts, + "option", "ManagedServiceAccountsEnabled", + ) return } ctxLogger.Debug("Routing the External Service registration to the External Service Account service", "service", cmd.Name) @@ -160,7 +170,7 @@ func (r *Registry) SaveExternalService(ctx context.Context, cmd *extsvcauth.Exte // retrieveExtSvcProviders fetches external services from store and map their associated provider func (r *Registry) retrieveExtSvcProviders(ctx context.Context) (map[string]extsvcauth.AuthProvider, error) { extsvcs := map[string]extsvcauth.AuthProvider{} - if r.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if r.enabled { names, err := r.saReg.GetExternalServiceNames(ctx) if err != nil { return nil, err diff --git a/pkg/services/extsvcauth/registry/service_test.go b/pkg/services/extsvcauth/registry/service_test.go index 40c046f4ceb..30b5ef81138 100644 --- a/pkg/services/extsvcauth/registry/service_test.go +++ b/pkg/services/extsvcauth/registry/service_test.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/extsvcauth/tests" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -30,7 +29,7 @@ func setupTestEnv(t *testing.T) *TestEnv { env := TestEnv{} env.saReg = tests.NewExternalServiceRegistryMock(t) env.r = &Registry{ - features: featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts), + enabled: true, logger: log.New("extsvcauth.registry.test"), saReg: env.saReg, extSvcProviders: map[string]extsvcauth.AuthProvider{}, diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index c87b872e64f..040fae2c88a 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -32,14 +32,14 @@ var ( Description: "This will use a webworker thread to processes events rather than the main thread", Stage: FeatureStageExperimental, FrontendOnly: true, - Owner: grafanaAppPlatformSquad, + Owner: grafanaDashboardsSquad, }, { Name: "queryOverLive", Description: "Use Grafana Live WebSocket to execute backend queries", Stage: FeatureStageExperimental, FrontendOnly: true, - Owner: grafanaAppPlatformSquad, + Owner: grafanaDashboardsSquad, }, { Name: "panelTitleSearch", @@ -67,9 +67,10 @@ var ( { Name: "publicDashboardsScene", Description: "Enables public dashboard rendering using scenes", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaSharingSquad, + Expression: "true", // enabled by default }, { Name: "lokiExperimentalStreaming", @@ -749,6 +750,12 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaSearchAndStorageSquad, }, + { + Name: "grafanaAPIServerTestingWithExperimentalAPIs", + Description: "Facilitate integration testing of experimental APIs", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + }, { Name: "datasourceQueryTypes", Description: "Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus)", @@ -888,23 +895,26 @@ var ( { Name: "dashboardSceneForViewers", Description: "Enables dashboard rendering using Scenes for viewer roles", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaDashboardsSquad, + Expression: "true", // enabled by default }, { Name: "dashboardSceneSolo", Description: "Enables rendering dashboards using scenes for solo panels", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaDashboardsSquad, + Expression: "true", // enabled by default }, { Name: "dashboardScene", Description: "Enables dashboard rendering using scenes for all roles", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: grafanaDashboardsSquad, + Expression: "true", // enabled by default }, { Name: "panelFilterVariable", @@ -1233,9 +1243,10 @@ var ( { Name: "notificationBanner", Description: "Enables the notification banner UI and API", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, Owner: grafanaFrontendPlatformSquad, FrontendOnly: false, + Expression: "true", }, { Name: "dashboardRestore", @@ -1292,8 +1303,9 @@ var ( { Name: "pinNavItems", Description: "Enables pinning of nav items", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, Owner: grafanaFrontendPlatformSquad, + Expression: "true", // enabled by default }, { Name: "authZGRPCServer", @@ -1403,6 +1415,13 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaObservabilityLogsSquad, }, + { + Name: "alertingPrometheusRulesPrimary", + Description: "Uses Prometheus rules as the primary source of truth for ruler-enabled data sources", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + FrontendOnly: true, + }, { Name: "singleTopNav", Description: "Unifies the top search bar and breadcrumb bar into one", @@ -1486,6 +1505,14 @@ var ( Stage: FeatureStageExperimental, Owner: identityAccessTeam, }, + { + Name: "unifiedStorageSearch", + Description: "Enable unified storage search", + Stage: FeatureStageExperimental, + Owner: grafanaSearchAndStorageSquad, + HideFromDocs: true, + HideFromAdminPage: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index ae8b748ca22..2be708e1aa2 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -1,11 +1,11 @@ Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly disableEnvelopeEncryption,GA,@grafana/grafana-as-code,false,false,false -live-service-web-worker,experimental,@grafana/grafana-app-platform-squad,false,false,true -queryOverLive,experimental,@grafana/grafana-app-platform-squad,false,false,true +live-service-web-worker,experimental,@grafana/dashboards-squad,false,false,true +queryOverLive,experimental,@grafana/dashboards-squad,false,false,true panelTitleSearch,preview,@grafana/search-and-storage,false,false,false publicDashboards,GA,@grafana/sharing-squad,false,false,false publicDashboardsEmailSharing,preview,@grafana/sharing-squad,false,false,false -publicDashboardsScene,experimental,@grafana/sharing-squad,false,false,true +publicDashboardsScene,GA,@grafana/sharing-squad,false,false,true lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,false featureHighlights,GA,@grafana/grafana-as-code,false,false,false storage,experimental,@grafana/search-and-storage,false,false,false @@ -98,6 +98,7 @@ 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 kubernetesFolders,experimental,@grafana/search-and-storage,false,false,false +grafanaAPIServerTestingWithExperimentalAPIs,experimental,@grafana/search-and-storage,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 @@ -117,9 +118,9 @@ alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false extractFieldsNameDeduplication,experimental,@grafana/dataviz-squad,false,false,true -dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,true -dashboardSceneSolo,experimental,@grafana/dashboards-squad,false,false,true -dashboardScene,experimental,@grafana/dashboards-squad,false,false,true +dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true +dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true +dashboardScene,GA,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/sharing-squad,false,false,false ssoSettingsApi,GA,@grafana/identity-access-team,false,false,false @@ -161,7 +162,7 @@ queryLibrary,experimental,@grafana/explore-squad,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true -notificationBanner,experimental,@grafana/grafana-frontend-platform,false,false,false +notificationBanner,GA,@grafana/grafana-frontend-platform,false,false,false dashboardRestore,experimental,@grafana/search-and-storage,false,false,false datasourceProxyDisableRBAC,GA,@grafana/identity-access-team,false,false,false alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false @@ -169,7 +170,7 @@ preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,fals alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true pluginProxyPreserveTrailingSlash,GA,@grafana/plugins-platform-backend,false,false,false azureMonitorPrometheusExemplars,preview,@grafana/partner-datasources,false,false,false -pinNavItems,experimental,@grafana/grafana-frontend-platform,false,false,false +pinNavItems,GA,@grafana/grafana-frontend-platform,false,false,false authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false openSearchBackendFlowEnabled,GA,@grafana/aws-datasources,false,false,false ssoSettingsLDAP,experimental,@grafana/identity-access-team,false,false,false @@ -185,6 +186,7 @@ alertingFilterV2,experimental,@grafana/alerting-squad,false,false,false dataplaneAggregator,experimental,@grafana/grafana-app-platform-squad,false,true,false newFiltersUI,experimental,@grafana/dashboards-squad,false,false,false lokiSendDashboardPanelNames,experimental,@grafana/observability-logs,false,false,false +alertingPrometheusRulesPrimary,experimental,@grafana/alerting-squad,false,false,true singleTopNav,experimental,@grafana/grafana-frontend-platform,false,false,true exploreLogsShardSplitting,experimental,@grafana/observability-logs,false,false,true exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,false,true @@ -197,3 +199,4 @@ alertingQueryAndExpressionsStepMode,experimental,@grafana/alerting-squad,false,f improvedExternalSessionHandling,experimental,@grafana/identity-access-team,false,false,false useSessionStorageForRedirection,preview,@grafana/identity-access-team,false,false,false rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false +unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 2be496880cb..d6df6fbfae8 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -403,6 +403,10 @@ const ( // Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s FlagKubernetesFolders = "kubernetesFolders" + // FlagGrafanaAPIServerTestingWithExperimentalAPIs + // Facilitate integration testing of experimental APIs + FlagGrafanaAPIServerTestingWithExperimentalAPIs = "grafanaAPIServerTestingWithExperimentalAPIs" + // FlagDatasourceQueryTypes // Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) FlagDatasourceQueryTypes = "datasourceQueryTypes" @@ -751,6 +755,10 @@ const ( // Send dashboard and panel names to Loki when querying FlagLokiSendDashboardPanelNames = "lokiSendDashboardPanelNames" + // FlagAlertingPrometheusRulesPrimary + // Uses Prometheus rules as the primary source of truth for ruler-enabled data sources + FlagAlertingPrometheusRulesPrimary = "alertingPrometheusRulesPrimary" + // FlagSingleTopNav // Unifies the top search bar and breadcrumb bar into one FlagSingleTopNav = "singleTopNav" @@ -798,4 +806,8 @@ const ( // FlagRolePickerDrawer // Enables the new role picker drawer design FlagRolePickerDrawer = "rolePickerDrawer" + + // FlagUnifiedStorageSearch + // Enable unified storage search + FlagUnifiedStorageSearch = "unifiedStorageSearch" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 24e8ef6522f..5ae6b05eb1e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -240,6 +240,22 @@ "hideFromAdminPage": true } }, + { + "metadata": { + "name": "alertingPrometheusRulesPrimary", + "resourceVersion": "1727332930692", + "creationTimestamp": "2024-09-09T13:56:47Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 06:42:10.692959 +0000 UTC" + } + }, + "spec": { + "description": "Uses Prometheus rules as the primary source of truth for ruler-enabled data sources", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "frontend": true + } + }, { "metadata": { "name": "alertingQueryAndExpressionsStepMode", @@ -824,40 +840,52 @@ { "metadata": { "name": "dashboardScene", - "resourceVersion": "1718727528075", - "creationTimestamp": "2023-11-13T08:51:21Z" + "resourceVersion": "1727354524763", + "creationTimestamp": "2023-11-13T08:51:21Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC" + } }, "spec": { "description": "Enables dashboard rendering using scenes for all roles", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/dashboards-squad", - "frontend": true + "frontend": true, + "expression": "true" } }, { "metadata": { "name": "dashboardSceneForViewers", - "resourceVersion": "1718727528075", - "creationTimestamp": "2023-11-02T19:02:25Z" + "resourceVersion": "1727354524763", + "creationTimestamp": "2023-11-02T19:02:25Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC" + } }, "spec": { "description": "Enables dashboard rendering using Scenes for viewer roles", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/dashboards-squad", - "frontend": true + "frontend": true, + "expression": "true" } }, { "metadata": { "name": "dashboardSceneSolo", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-02-11T08:08:47Z" + "resourceVersion": "1727354524763", + "creationTimestamp": "2024-02-11T08:08:47Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC" + } }, "spec": { "description": "Enables rendering dashboards using scenes for solo panels", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/dashboards-squad", - "frontend": true + "frontend": true, + "expression": "true" } }, { @@ -1356,6 +1384,18 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "grafanaAPIServerTestingWithExperimentalAPIs", + "resourceVersion": "1727945615419", + "creationTimestamp": "2024-10-03T08:53:35Z" + }, + "spec": { + "description": "Facilitate integration testing of experimental APIs", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage" + } + }, { "metadata": { "name": "grafanaAPIServerWithExperimentalAPIs", @@ -1662,13 +1702,16 @@ { "metadata": { "name": "live-service-web-worker", - "resourceVersion": "1718727528075", - "creationTimestamp": "2022-01-26T17:44:20Z" + "resourceVersion": "1727432782383", + "creationTimestamp": "2022-01-26T17:44:20Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-27 10:26:22.38366 +0000 UTC" + } }, "spec": { "description": "This will use a webworker thread to processes events rather than the main thread", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", + "codeowner": "@grafana/dashboards-squad", "frontend": true } }, @@ -2077,13 +2120,17 @@ { "metadata": { "name": "notificationBanner", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-05-13T09:32:34Z" + "resourceVersion": "1727777007488", + "creationTimestamp": "2024-05-13T09:32:34Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-10-01 10:03:27.48823 +0000 UTC" + } }, "spec": { "description": "Enables the notification banner UI and API", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform" + "stage": "GA", + "codeowner": "@grafana/grafana-frontend-platform", + "expression": "true" } }, { @@ -2233,13 +2280,17 @@ { "metadata": { "name": "pinNavItems", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-06-10T11:40:03Z" + "resourceVersion": "1727948775526", + "creationTimestamp": "2024-06-10T11:40:03Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-10-03 09:46:15.526594 +0000 UTC" + } }, "spec": { "description": "Enables pinning of nav items", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform" + "stage": "GA", + "codeowner": "@grafana/grafana-frontend-platform", + "expression": "true" } }, { @@ -2497,14 +2548,18 @@ { "metadata": { "name": "publicDashboardsScene", - "resourceVersion": "1718727528075", - "creationTimestamp": "2024-03-22T14:48:21Z" + "resourceVersion": "1727354524763", + "creationTimestamp": "2024-03-22T14:48:21Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC" + } }, "spec": { "description": "Enables public dashboard rendering using scenes", - "stage": "experimental", + "stage": "GA", "codeowner": "@grafana/sharing-squad", - "frontend": true + "frontend": true, + "expression": "true" } }, { @@ -2523,13 +2578,16 @@ { "metadata": { "name": "queryOverLive", - "resourceVersion": "1718727528075", - "creationTimestamp": "2022-01-26T17:44:20Z" + "resourceVersion": "1727432782383", + "creationTimestamp": "2022-01-26T17:44:20Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-09-27 10:26:22.38366 +0000 UTC" + } }, "spec": { "description": "Use Grafana Live WebSocket to execute backend queries", "stage": "experimental", - "codeowner": "@grafana/grafana-app-platform-squad", + "codeowner": "@grafana/dashboards-squad", "frontend": true } }, @@ -2961,6 +3019,20 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "unifiedStorageSearch", + "resourceVersion": "1726771421439", + "creationTimestamp": "2024-09-19T18:43:41Z" + }, + "spec": { + "description": "Enable unified storage search", + "stage": "experimental", + "codeowner": "@grafana/search-and-storage", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "useSeessionStorageForRedirection", diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 48cc32b7b76..36f21e37170 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -11,6 +11,7 @@ import ( "sync" "time" + "github.com/grafana/authlib/claims" "github.com/grafana/dskit/concurrency" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel/attribute" @@ -29,23 +30,27 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) const FULLPATH_SEPARATOR = "/" type Service struct { - store store + store folder.Store db db.DB log *slog.Logger dashboardStore dashboards.Store dashboardFolderStore folder.FolderStore features featuremgmt.FeatureToggles + cfg *setting.Cfg + folderPermissions accesscontrol.FolderPermissionsService accessControl accesscontrol.AccessControl // bus is currently used to publish event in case of folder full path change. // For example when a folder is moved to another folder or when a folder is renamed. @@ -58,23 +63,27 @@ type Service struct { } func ProvideService( + store *FolderStoreImpl, ac accesscontrol.AccessControl, bus bus.Bus, dashboardStore dashboards.Store, folderStore folder.FolderStore, db db.DB, // DB for the (new) nested folder store features featuremgmt.FeatureToggles, + cfg *setting.Cfg, + folderPermissions accesscontrol.FolderPermissionsService, supportBundles supportbundles.Service, r prometheus.Registerer, tracer tracing.Tracer, ) folder.Service { - store := ProvideStore(db) srv := &Service{ log: slog.Default().With("logger", "folder-service"), dashboardStore: dashboardStore, dashboardFolderStore: folderStore, store: store, features: features, + cfg: cfg, + folderPermissions: folderPermissions, accessControl: ac, bus: bus, db: db, @@ -86,9 +95,9 @@ func ProvideService( supportBundles.RegisterSupportItemCollector(srv.supportBundleCollector()) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(folderStore, srv)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(folderStore, store)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, store)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(store)) return srv } @@ -143,10 +152,10 @@ func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]* return nil, folder.ErrBadRequest.Errorf("missing signed in user") } - qry := NewGetFoldersQuery(q) + qry := folder.NewGetFoldersQuery(q) permissions := q.SignedInUser.GetPermissions() folderPermissions := permissions[dashboards.ActionFoldersRead] - qry.ancestorUIDs = make([]string, 0, len(folderPermissions)) + qry.AncestorUIDs = make([]string, 0, len(folderPermissions)) if len(folderPermissions) == 0 && !q.SignedInUser.GetIsGrafanaAdmin() { return nil, nil } @@ -154,12 +163,12 @@ func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]* if p == dashboards.ScopeFoldersAll { // no need to query for folders with permissions // the user has permission to access all folders - qry.ancestorUIDs = nil + qry.AncestorUIDs = nil break } if folderUid, found := strings.CutPrefix(p, dashboards.ScopeFoldersPrefix); found { - if !slices.Contains(qry.ancestorUIDs, folderUid) { - qry.ancestorUIDs = append(qry.ancestorUIDs, folderUid) + if !slices.Contains(qry.AncestorUIDs, folderUid) { + qry.AncestorUIDs = append(qry.AncestorUIDs, folderUid) } } } @@ -658,9 +667,43 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( if nestedFolder != nil && nestedFolder.ParentUID != "" { f.ParentUID = nestedFolder.ParentUID } + if err = s.setDefaultFolderPermissions(ctx, cmd.OrgID, user, f); err != nil { + return nil, err + } + return f, nil } +func (s *Service) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, folder *folder.Folder) error { + if !s.cfg.RBAC.PermissionsOnCreation("folder") { + return nil + } + + var permissions []accesscontrol.SetResourcePermissionCommand + + if user.IsIdentityType(claims.TypeUser) { + userID, err := user.GetInternalID() + if err != nil { + return err + } + + permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ + UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(), + }) + } + + isNested := folder.ParentUID != "" + if !isNested || !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) { + permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{ + {BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()}, + {BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()}, + }...) + } + + _, err := s.folderPermissions.SetPermissions(ctx, orgID, folder.UID, permissions...) + return err +} + func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) { ctx, span := s.tracer.Start(ctx, "folder.Update") defer span.End() diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 7ad8b4b8235..283dcb668c1 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -62,8 +62,11 @@ func TestIntegrationProvideFolderService(t *testing.T) { } t.Run("should register scope resolvers", func(t *testing.T) { ac := acmock.New() - db := db.InitTestDB(t) - ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + db, cfg := db.InitTestDBWithCfg(t) + folderPermissions := acmock.NewMockedPermissionsService() + store := ProvideStore(db) + ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db, + featuremgmt.WithFeatures(), cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3) }) @@ -95,6 +98,7 @@ func TestIntegrationFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, + cfg: cfg, bus: bus.ProvideBus(tracing.InitializeTracerForTest()), db: db, accessControl: acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), @@ -437,6 +441,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOn, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -486,13 +491,13 @@ func TestIntegrationNestedFolderService(t *testing.T) { CanEditValue: true, }) - dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nil) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac) require.NoError(t, err) - elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, featuresFlagOn, ac) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOn, serviceWithFlagOn.store, featuresFlagOn, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOn) require.NoError(t, err) @@ -552,6 +557,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOff, + cfg: cfg, bus: b, db: db, registry: make(map[string]folder.RegistryService), @@ -568,13 +574,13 @@ func TestIntegrationNestedFolderService(t *testing.T) { }) dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff, - folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nil) + folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac) require.NoError(t, err) - elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, featuresFlagOff, ac) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, serviceWithFlagOff, serviceWithFlagOff.store, featuresFlagOff, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, serviceWithFlagOff) require.NoError(t, err) @@ -630,6 +636,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"), dashboardFolderStore: folderStore, features: featuresFlagOff, + cfg: cfg, bus: b, db: db, registry: make(map[string]folder.RegistryService), @@ -703,7 +710,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { CanEditValue: true, }) - elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.featuresFlag, ac) + elementService := libraryelements.ProvideService(cfg, db, routeRegister, tc.service, tc.service.store, tc.featuresFlag, ac) lps, err := librarypanels.ProvideService(cfg, db, routeRegister, elementService, tc.service) require.NoError(t, err) @@ -713,7 +720,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { tc.service.dashboardStore = dashStore tc.service.store = nestedFolderStore - dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, nil) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac) require.NoError(t, err) @@ -792,7 +799,7 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) { guardian.New = g }) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() dashStore := dashboards.FakeDashboardStore{} dashStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.AnythingOfType("*dashboards.Dashboard"), mock.AnythingOfType("bool")).Return(true, nil) @@ -808,6 +815,7 @@ func TestNestedFolderServiceFeatureToggle(t *testing.T) { dashboardStore: &dashStore, dashboardFolderStore: dashboardFolderStore, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + cfg: setting.NewCfg(), accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), @@ -845,6 +853,7 @@ func TestFolderServiceDualWrite(t *testing.T) { dashboardStore: dashStore, dashboardFolderStore: dashboardFolderStore, features: featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders), + cfg: cfg, accessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), metrics: newFoldersMetrics(nil), tracer: tracing.InitializeTracerForTest(), @@ -906,7 +915,7 @@ func TestNestedFolderService(t *testing.T) { dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() features := featuremgmt.WithFeatures() db, _ := sqlstore.InitTestDB(t) @@ -944,7 +953,7 @@ func TestNestedFolderService(t *testing.T) { dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(dash, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() features := featuremgmt.WithFeatures("nestedFolders") tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} @@ -974,7 +983,7 @@ func TestNestedFolderService(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() features := featuremgmt.WithFeatures("nestedFolders") tempUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} @@ -1044,7 +1053,7 @@ func TestNestedFolderService(t *testing.T) { nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_parent")}} - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() db, _ := sqlstore.InitTestDB(t) features := featuremgmt.WithFeatures("nestedFolders") folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), db) @@ -1085,7 +1094,7 @@ func TestNestedFolderService(t *testing.T) { dashStore.On("SaveDashboard", mock.Anything, mock.AnythingOfType("dashboards.SaveDashboardCommand")).Return(&dashboards.Dashboard{UID: "newUID"}, nil) dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() db, _ := sqlstore.InitTestDB(t) folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ @@ -1122,7 +1131,7 @@ func TestNestedFolderService(t *testing.T) { dashboardFolderStore := foldertest.NewFakeFolderStore(t) dashboardFolderStore.On("GetFolderByUID", mock.Anything, orgID, dashboardFolder.UID).Return(f, nil) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedParentFolders = []*folder.Folder{ {UID: "newFolder", ParentUID: "newFolder"}, {UID: "newFolder2", ParentUID: "newFolder2"}, @@ -1164,7 +1173,7 @@ func TestNestedFolderService(t *testing.T) { dashboardFolderStore := foldertest.NewFakeFolderStore(t) // return an error from the folder store - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedError = errors.New("FAILED") // the service return success as long as the legacy create succeeds @@ -1189,7 +1198,7 @@ func TestNestedFolderService(t *testing.T) { dashboardFolderStore := foldertest.NewFakeFolderStore(t) //dashboardFolderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(&folder.Folder{}, nil) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} @@ -1205,7 +1214,7 @@ func TestNestedFolderService(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = []*folder.Folder{ {UID: "newFolder", ParentUID: "newFolder"}, @@ -1236,7 +1245,7 @@ func TestNestedFolderService(t *testing.T) { nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}} features := featuremgmt.WithFeatures("nestedFolders") - folderSvc := setup(t, &dashboards.FakeDashboardStore{}, foldertest.NewFakeFolderStore(t), NewFakeStore(), features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) + folderSvc := setup(t, &dashboards.FakeDashboardStore{}, foldertest.NewFakeFolderStore(t), folder.NewFakeStore(), features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), dbtest.NewFakeDB()) _, err := folderSvc.Move(context.Background(), &folder.MoveFolderCommand{UID: accesscontrol.K6FolderUID, NewParentUID: "newFolder", OrgID: orgID, SignedInUser: nestedFolderUser}) require.Error(t, err, folder.ErrBadRequest) }) @@ -1246,7 +1255,7 @@ func TestNestedFolderService(t *testing.T) { nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersWrite: {dashboards.ScopeFoldersProvider.GetResourceAllScope()}} childUID := "k6-app-child" - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{ OrgID: orgID, UID: childUID, @@ -1263,7 +1272,7 @@ func TestNestedFolderService(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} @@ -1279,7 +1288,7 @@ func TestNestedFolderService(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = []*folder.Folder{ {UID: "newFolder", ParentUID: "newFolder"}, @@ -1307,7 +1316,7 @@ func TestNestedFolderService(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderUser := &user.SignedInUser{UserID: 1, OrgID: orgID, Permissions: map[int64]map[string][]string{}} nestedFolderUser.Permissions[orgID] = map[string][]string{dashboards.ActionFoldersCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("some_subfolder")}} @@ -1328,7 +1337,7 @@ func TestNestedFolderService(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedError = folder.ErrCircularReference @@ -1350,7 +1359,7 @@ func TestNestedFolderService(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = []*folder.Folder{ {UID: "newFolder", ParentUID: "newFolder"}, @@ -1376,7 +1385,7 @@ func TestNestedFolderService(t *testing.T) { dashStore := &dashboards.FakeDashboardStore{} dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = []*folder.Folder{{UID: "myFolder", ParentUID: "12345"}, {UID: "12345", ParentUID: ""}} @@ -1409,7 +1418,7 @@ func TestNestedFolderService(t *testing.T) { parents = append(parents, &folder.Folder{UID: fmt.Sprintf("folder%d", i)}) } - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() //nestedFolderStore.ExpectedFolder = &folder.Folder{UID: "myFolder", ParentUID: "newFolder"} nestedFolderStore.ExpectedParentFolders = parents @@ -1439,7 +1448,7 @@ func TestNestedFolderService(t *testing.T) { dashboardFolderStore := foldertest.NewFakeFolderStore(t) - nestedFolderStore := NewFakeStore() + nestedFolderStore := folder.NewFakeStore() nestedFolderStore.ExpectedError = folder.ErrFolderNotFound folderSvc := setup(t, dashStore, dashboardFolderStore, nestedFolderStore, featuremgmt.WithFeatures("nestedFolders"), actest.FakeAccessControl{ @@ -1477,6 +1486,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOn, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -1493,6 +1503,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { dashboardPermissions, actest.FakeAccessControl{}, serviceWithFlagOn, + nestedFolderStore, nil, ) require.NoError(t, err) @@ -1898,6 +1909,7 @@ func TestFolderServiceGetFolder(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -1980,6 +1992,7 @@ func TestFolderServiceGetFolders(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: featuresFlagOff, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -2067,6 +2080,7 @@ func TestGetChildrenFilterByPermission(t *testing.T) { dashboardFolderStore: folderStore, store: nestedFolderStore, features: features, + cfg: cfg, bus: b, db: db, accessControl: ac, @@ -2496,7 +2510,7 @@ func TestSupportBundle(t *testing.T) { } } -func CreateSubtreeInStore(t *testing.T, store store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder { +func CreateSubtreeInStore(t *testing.T, store folder.Store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder { t.Helper() folders := make([]*folder.Folder, 0, depth) @@ -2520,7 +2534,7 @@ func CreateSubtreeInStore(t *testing.T, store store, service *Service, depth int return folders } -func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, db db.DB) folder.Service { +func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder.FolderStore, nestedFolderStore folder.Store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, db db.DB) folder.Service { t.Helper() // nothing enabled yet @@ -2530,6 +2544,7 @@ func setup(t *testing.T, dashStore dashboards.Store, dashboardFolderStore folder dashboardFolderStore: dashboardFolderStore, store: nestedFolderStore, features: features, + cfg: setting.NewCfg(), accessControl: ac, db: db, metrics: newFoldersMetrics(nil), diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index c4464087840..2e085fddccd 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -22,19 +22,19 @@ import ( const DEFAULT_BATCH_SIZE = 999 -type sqlStore struct { +type FolderStoreImpl struct { db db.DB log log.Logger } // sqlStore implements the store interface. -var _ store = (*sqlStore)(nil) +var _ folder.Store = (*FolderStoreImpl)(nil) -func ProvideStore(db db.DB) *sqlStore { - return &sqlStore{db: db, log: log.New("folder-store")} +func ProvideStore(db db.DB) *FolderStoreImpl { + return &FolderStoreImpl{db: db, log: log.New("folder-store")} } -func (ss *sqlStore) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) { +func (ss *FolderStoreImpl) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) { if cmd.UID == "" { return nil, folder.ErrBadRequest.Errorf("missing UID") } @@ -83,7 +83,7 @@ func (ss *sqlStore) Create(ctx context.Context, cmd folder.CreateFolderCommand) return foldr.WithURL(), err } -func (ss *sqlStore) Delete(ctx context.Context, UIDs []string, orgID int64) error { +func (ss *FolderStoreImpl) Delete(ctx context.Context, UIDs []string, orgID int64) error { if len(UIDs) == 0 { return nil } @@ -103,7 +103,7 @@ func (ss *sqlStore) Delete(ctx context.Context, UIDs []string, orgID int64) erro }) } -func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) { +func (ss *FolderStoreImpl) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) { updated := time.Now() uid := cmd.UID @@ -191,7 +191,7 @@ func (ss *sqlStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand) // └── B/C // // The full path of C is "A/B\/C". -func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) { +func (ss *FolderStoreImpl) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) { foldr := &folder.Folder{} err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { exists := false @@ -244,7 +244,7 @@ func (ss *sqlStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.F return foldr.WithURL(), err } -func (ss *sqlStore) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { +func (ss *FolderStoreImpl) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { if q.UID == "" { return []*folder.Folder{}, nil } @@ -295,7 +295,7 @@ func (ss *sqlStore) GetParents(ctx context.Context, q folder.GetParentsQuery) ([ return util.Reverse(folders[1:]), nil } -func (ss *sqlStore) GetChildren(ctx context.Context, q folder.GetChildrenQuery) ([]*folder.Folder, error) { +func (ss *FolderStoreImpl) GetChildren(ctx context.Context, q folder.GetChildrenQuery) ([]*folder.Folder, error) { var folders []*folder.Folder err := ss.db.WithDbSession(ctx, func(sess *db.Session) error { @@ -353,7 +353,7 @@ func (ss *sqlStore) GetChildren(ctx context.Context, q folder.GetChildrenQuery) return folders, err } -func (ss *sqlStore) getParentsMySQL(ctx context.Context, q folder.GetParentsQuery) (folders []*folder.Folder, err error) { +func (ss *FolderStoreImpl) getParentsMySQL(ctx context.Context, q folder.GetParentsQuery) (folders []*folder.Folder, err error) { err = ss.db.WithDbSession(ctx, func(sess *db.Session) error { uid := "" // covered by UQE_folder_org_id_uid @@ -387,7 +387,7 @@ func (ss *sqlStore) getParentsMySQL(ctx context.Context, q folder.GetParentsQuer } // TODO use a single query to get the height of a folder -func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) { +func (ss *FolderStoreImpl) GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) { height := -1 queue := []string{foldrUID} for len(queue) > 0 && height <= folder.MaxNestedFolderDepth { @@ -445,7 +445,7 @@ func (ss *sqlStore) GetHeight(ctx context.Context, foldrUID string, orgID int64, // The full path UIDs of C is "uid1/uid2/uid3". // The full path UIDs of B is "uid1/uid2". // The full path UIDs of A is "uid1". -func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) { +func (ss *FolderStoreImpl) GetFolders(ctx context.Context, q folder.GetFoldersFromStoreQuery) ([]*folder.Folder, error) { if q.BatchSize == 0 { q.BatchSize = DEFAULT_BATCH_SIZE } @@ -467,7 +467,7 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde } s.WriteString(` FROM folder f0`) // join the same table multiple times to compute the full path of a folder - if q.WithFullpath || q.WithFullpathUIDs || len(q.ancestorUIDs) > 0 { + if q.WithFullpath || q.WithFullpathUIDs || len(q.AncestorUIDs) > 0 { s.WriteString(getFullpathJoinsSQL()) } // covered by UQE_folder_org_id_uid @@ -489,7 +489,7 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde args = append(args, accesscontrol.K6FolderUID, accesscontrol.K6FolderUID) } - if len(q.ancestorUIDs) == 0 { + if len(q.AncestorUIDs) == 0 { if q.OrderByTitle { s.WriteString(` ORDER BY f0.title ASC`) } @@ -503,8 +503,8 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde } // filter out folders if they are not in the subtree of the given ancestor folders - if err := batch(len(q.ancestorUIDs), int(q.BatchSize), func(start2, end2 int) error { - s2, args2 := getAncestorsSQL(ss.db.GetDialect(), q.ancestorUIDs, start2, end2, s.String(), args) + if err := batch(len(q.AncestorUIDs), int(q.BatchSize), func(start2, end2 int) error { + s2, args2 := getAncestorsSQL(ss.db.GetDialect(), q.AncestorUIDs, start2, end2, s.String(), args) if q.OrderByTitle { s2 += " ORDER BY f0.title ASC" } @@ -533,7 +533,7 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde return folders, nil } -func (ss *sqlStore) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) { +func (ss *FolderStoreImpl) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) { var folders []*folder.Folder recursiveQueriesAreSupported, err := ss.db.RecursiveQueriesAreSupported() diff --git a/pkg/services/folder/folderimpl/sqlstore_test.go b/pkg/services/folder/folderimpl/sqlstore_test.go index c966bbf679b..dc60826d618 100644 --- a/pkg/services/folder/folderimpl/sqlstore_test.go +++ b/pkg/services/folder/folderimpl/sqlstore_test.go @@ -865,7 +865,7 @@ func TestIntegrationGetFolders(t *testing.T) { }) t.Run("get folders by UIDs should succeed", func(t *testing.T) { - actualFolders, err := folderStore.GetFolders(context.Background(), NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:]})) + actualFolders, err := folderStore.GetFolders(context.Background(), folder.NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:]})) require.NoError(t, err) assert.Equal(t, len(uids[1:]), len(actualFolders)) for _, f := range folders[1:] { @@ -885,7 +885,7 @@ func TestIntegrationGetFolders(t *testing.T) { }) t.Run("get folders by UIDs batching should work as expected", func(t *testing.T) { - q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], BatchSize: 3}) + q := folder.NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], BatchSize: 3}) actualFolders, err := folderStore.GetFolders(context.Background(), q) require.NoError(t, err) assert.Equal(t, len(uids[1:]), len(actualFolders)) @@ -906,7 +906,7 @@ func TestIntegrationGetFolders(t *testing.T) { }) t.Run("get folders by UIDs with fullpath should succeed", func(t *testing.T) { - q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], WithFullpath: true}) + q := folder.NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], WithFullpath: true}) q.BatchSize = 3 actualFolders, err := folderStore.GetFolders(context.Background(), q) require.NoError(t, err) @@ -929,12 +929,12 @@ func TestIntegrationGetFolders(t *testing.T) { }) t.Run("get folders by UIDs and ancestor UIDs should work as expected", func(t *testing.T) { - q := NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], BatchSize: 3}) - q.ancestorUIDs = make([]string, 0, int(q.BatchSize)+1) + q := folder.NewGetFoldersQuery(folder.GetFoldersQuery{OrgID: orgID, UIDs: uids[1:], BatchSize: 3}) + q.AncestorUIDs = make([]string, 0, int(q.BatchSize)+1) for i := 0; i < int(q.BatchSize); i++ { - q.ancestorUIDs = append(q.ancestorUIDs, uuid.New().String()) + q.AncestorUIDs = append(q.AncestorUIDs, uuid.New().String()) } - q.ancestorUIDs = append(q.ancestorUIDs, folders[len(folders)-1].UID) + q.AncestorUIDs = append(q.AncestorUIDs, folders[len(folders)-1].UID) actualFolders, err := folderStore.GetFolders(context.Background(), q) require.NoError(t, err) @@ -967,7 +967,7 @@ func CreateOrg(t *testing.T, db db.DB, cfg *setting.Cfg) int64 { return orgID } -func CreateSubtree(t *testing.T, store *sqlStore, orgID int64, parentUID string, depth int, prefix string) []string { +func CreateSubtree(t *testing.T, store *FolderStoreImpl, orgID int64, parentUID string, depth int, prefix string) []string { t.Helper() ancestorUIDs := []string{} @@ -1006,7 +1006,7 @@ func CreateSubtree(t *testing.T, store *sqlStore, orgID int64, parentUID string, return ancestorUIDs } -func CreateLeaves(t *testing.T, store *sqlStore, parent *folder.Folder, num int) []string { +func CreateLeaves(t *testing.T, store *FolderStoreImpl, parent *folder.Folder, num int) []string { t.Helper() leaves := make([]string, 0) @@ -1024,7 +1024,7 @@ func CreateLeaves(t *testing.T, store *sqlStore, parent *folder.Folder, num int) return leaves } -func assertAncestorUIDs(t *testing.T, store *sqlStore, f *folder.Folder, expected []string) { +func assertAncestorUIDs(t *testing.T, store *FolderStoreImpl, f *folder.Folder, expected []string) { t.Helper() ancestors, err := store.GetParents(context.Background(), folder.GetParentsQuery{ @@ -1039,7 +1039,7 @@ func assertAncestorUIDs(t *testing.T, store *sqlStore, f *folder.Folder, expecte assert.Equal(t, expected, actualAncestorsUIDs) } -func assertChildrenUIDs(t *testing.T, store *sqlStore, f *folder.Folder, expected []string) { +func assertChildrenUIDs(t *testing.T, store *FolderStoreImpl, f *folder.Folder, expected []string) { t.Helper() ancestors, err := store.GetChildren(context.Background(), folder.GetChildrenQuery{ diff --git a/pkg/services/folder/folderimpl/store_fake.go b/pkg/services/folder/folderimpl/store_fake.go deleted file mode 100644 index 33af4ebed09..00000000000 --- a/pkg/services/folder/folderimpl/store_fake.go +++ /dev/null @@ -1,66 +0,0 @@ -package folderimpl - -import ( - "context" - - "github.com/grafana/grafana/pkg/services/folder" -) - -type fakeStore struct { - ExpectedChildFolders []*folder.Folder - ExpectedParentFolders []*folder.Folder - ExpectedFolders []*folder.Folder - ExpectedFolder *folder.Folder - ExpectedError error - ExpectedFolderHeight int - CreateCalled bool - DeleteCalled bool -} - -func NewFakeStore() *fakeStore { - return &fakeStore{} -} - -var _ store = (*fakeStore)(nil) - -func (f *fakeStore) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) { - f.CreateCalled = true - return f.ExpectedFolder, f.ExpectedError -} - -func (f *fakeStore) Delete(ctx context.Context, UIDs []string, orgID int64) error { - f.DeleteCalled = true - return f.ExpectedError -} - -func (f *fakeStore) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) { - return f.ExpectedFolder, f.ExpectedError -} - -func (f *fakeStore) Move(ctx context.Context, cmd folder.MoveFolderCommand) error { - return f.ExpectedError -} - -func (f *fakeStore) Get(ctx context.Context, cmd folder.GetFolderQuery) (*folder.Folder, error) { - return f.ExpectedFolder, f.ExpectedError -} - -func (f *fakeStore) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) { - return f.ExpectedParentFolders, f.ExpectedError -} - -func (f *fakeStore) GetChildren(ctx context.Context, cmd folder.GetChildrenQuery) ([]*folder.Folder, error) { - return f.ExpectedChildFolders, f.ExpectedError -} - -func (f *fakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64, parentUID *string) (int, error) { - return f.ExpectedFolderHeight, f.ExpectedError -} - -func (f *fakeStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) { - return f.ExpectedFolders, f.ExpectedError -} - -func (f *fakeStore) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) { - return f.ExpectedFolders, f.ExpectedError -} diff --git a/pkg/services/folder/model.go b/pkg/services/folder/model.go index 309c4cc15d4..b0869713f30 100644 --- a/pkg/services/folder/model.go +++ b/pkg/services/folder/model.go @@ -54,7 +54,7 @@ type Folder struct { } var GeneralFolder = Folder{ID: 0, Title: "General"} -var RootFolder = &Folder{ID: 0, Title: "Dashbboards", UID: GeneralFolderUID, ParentUID: ""} +var RootFolder = &Folder{ID: 0, Title: "Dashboards", UID: GeneralFolderUID, ParentUID: ""} var SharedWithMeFolder = Folder{ Title: "Shared with me", Description: "Dashboards and folders shared with me", diff --git a/pkg/services/folder/folderimpl/store.go b/pkg/services/folder/store.go similarity index 59% rename from pkg/services/folder/folderimpl/store.go rename to pkg/services/folder/store.go index d2177ae4bfb..e2f17986221 100644 --- a/pkg/services/folder/folderimpl/store.go +++ b/pkg/services/folder/store.go @@ -1,27 +1,25 @@ -package folderimpl +package folder import ( "context" - - "github.com/grafana/grafana/pkg/services/folder" ) -type getFoldersQuery struct { - folder.GetFoldersQuery - ancestorUIDs []string +type GetFoldersFromStoreQuery struct { + GetFoldersQuery + AncestorUIDs []string } -func NewGetFoldersQuery(q folder.GetFoldersQuery) getFoldersQuery { - return getFoldersQuery{ +func NewGetFoldersQuery(q GetFoldersQuery) GetFoldersFromStoreQuery { + return GetFoldersFromStoreQuery{ GetFoldersQuery: q, - ancestorUIDs: []string{}, + AncestorUIDs: []string{}, } } -// store is the interface which a folder store must implement. -type store interface { +// Store is the interface which a folder Store must implement. +type Store interface { // Create creates a folder and returns the newly-created folder. - Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) + Create(ctx context.Context, cmd CreateFolderCommand) (*Folder, error) // Delete folders with the specified UIDs and orgID from the folder store. Delete(ctx context.Context, UIDs []string, orgID int64) error @@ -30,24 +28,24 @@ type store interface { // If the NewParentUID field is not nil, it updates also the parent UID (move mode). // If it's a non empty string, it moves the folder under the folder with the specific UID // otherwise, it moves the folder under the root folder (parent_uid column is set to NULL). - Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) + Update(ctx context.Context, cmd UpdateFolderCommand) (*Folder, error) // Get returns a folder. - Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) + Get(ctx context.Context, q GetFolderQuery) (*Folder, error) // GetParents returns an ordered list of parent folder of the given folder. - GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) + GetParents(ctx context.Context, q GetParentsQuery) ([]*Folder, error) // GetChildren returns the set of immediate children folders (depth=1) of the // given folder. - GetChildren(ctx context.Context, q folder.GetChildrenQuery) ([]*folder.Folder, error) + GetChildren(ctx context.Context, q GetChildrenQuery) ([]*Folder, error) // GetHeight returns the height of the folder tree. When parentUID is set, the function would // verify in the meanwhile that parentUID is not present in the subtree of the folder with the given UID. GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) // GetFolders returns folders with given uids - GetFolders(ctx context.Context, q getFoldersQuery) ([]*folder.Folder, error) + GetFolders(ctx context.Context, q GetFoldersFromStoreQuery) ([]*Folder, error) // GetDescendants returns all descendants of a folder - GetDescendants(ctx context.Context, orgID int64, anchestor_uid string) ([]*folder.Folder, error) + GetDescendants(ctx context.Context, orgID int64, anchestor_uid string) ([]*Folder, error) } diff --git a/pkg/services/folder/store_fake.go b/pkg/services/folder/store_fake.go new file mode 100644 index 00000000000..79f1a1d6863 --- /dev/null +++ b/pkg/services/folder/store_fake.go @@ -0,0 +1,64 @@ +package folder + +import ( + "context" +) + +type fakeStore struct { + ExpectedChildFolders []*Folder + ExpectedParentFolders []*Folder + ExpectedFolders []*Folder + ExpectedFolder *Folder + ExpectedError error + ExpectedFolderHeight int + CreateCalled bool + DeleteCalled bool +} + +func NewFakeStore() *fakeStore { + return &fakeStore{} +} + +var _ Store = (*fakeStore)(nil) + +func (f *fakeStore) Create(ctx context.Context, cmd CreateFolderCommand) (*Folder, error) { + f.CreateCalled = true + return f.ExpectedFolder, f.ExpectedError +} + +func (f *fakeStore) Delete(ctx context.Context, UIDs []string, orgID int64) error { + f.DeleteCalled = true + return f.ExpectedError +} + +func (f *fakeStore) Update(ctx context.Context, cmd UpdateFolderCommand) (*Folder, error) { + return f.ExpectedFolder, f.ExpectedError +} + +func (f *fakeStore) Move(ctx context.Context, cmd MoveFolderCommand) error { + return f.ExpectedError +} + +func (f *fakeStore) Get(ctx context.Context, cmd GetFolderQuery) (*Folder, error) { + return f.ExpectedFolder, f.ExpectedError +} + +func (f *fakeStore) GetParents(ctx context.Context, q GetParentsQuery) ([]*Folder, error) { + return f.ExpectedParentFolders, f.ExpectedError +} + +func (f *fakeStore) GetChildren(ctx context.Context, cmd GetChildrenQuery) ([]*Folder, error) { + return f.ExpectedChildFolders, f.ExpectedError +} + +func (f *fakeStore) GetHeight(ctx context.Context, folderUID string, orgID int64, parentUID *string) (int, error) { + return f.ExpectedFolderHeight, f.ExpectedError +} + +func (f *fakeStore) GetFolders(ctx context.Context, q GetFoldersFromStoreQuery) ([]*Folder, error) { + return f.ExpectedFolders, f.ExpectedError +} + +func (f *fakeStore) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*Folder, error) { + return f.ExpectedFolders, f.ExpectedError +} diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index ae6aaf279b7..ba8b19cd72f 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" "github.com/grafana/grafana/pkg/services/user" @@ -958,13 +959,13 @@ func setupAccessControlGuardianTest( fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Maybe().Return(d, nil) ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()) - folderSvc := foldertest.NewFakeService() + fStore := folder.NewFakeStore() folderStore := foldertest.NewFakeFolderStore(t) - ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, fakeDashboardService, folderSvc)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(folderSvc)) - ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, folderSvc)) + ac.RegisterScopeAttributeResolver(dashboards.NewDashboardUIDScopeResolver(folderStore, fakeDashboardService, fStore)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(fStore)) + ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, fStore)) license := licensingtest.NewFakeLicensing() license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() diff --git a/pkg/services/libraryelements/accesscontrol.go b/pkg/services/libraryelements/accesscontrol.go index 68d789cad06..9b52cdd2357 100644 --- a/pkg/services/libraryelements/accesscontrol.go +++ b/pkg/services/libraryelements/accesscontrol.go @@ -35,7 +35,7 @@ var ( // LibraryPanelUIDScopeResolver provides a ScopeAttributeResolver that is able to convert a scope prefixed with "library.panels:uid:" // into uid based scopes for a library panel and its associated folder hierarchy -func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderSvc folder.Service) (string, ac.ScopeAttributeResolver) { +func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderStore folder.Store) (string, ac.ScopeAttributeResolver) { prefix := ScopeLibraryPanelsProvider.GetResourceScopeUID("") return prefix, ac.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { if !strings.HasPrefix(scope, prefix) { @@ -60,7 +60,7 @@ func LibraryPanelUIDScopeResolver(l *LibraryElementService, folderSvc folder.Ser return nil, err } - inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, libElDTO.FolderUID, folderSvc) + inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, libElDTO.FolderUID, folderStore) if err != nil { return nil, err } diff --git a/pkg/services/libraryelements/fake/libraryelements_service.go b/pkg/services/libraryelements/fake/libraryelements_service.go new file mode 100644 index 00000000000..e8f0b3615d5 --- /dev/null +++ b/pkg/services/libraryelements/fake/libraryelements_service.go @@ -0,0 +1,125 @@ +package fake + +import ( + "context" + "sync" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/libraryelements/model" + "github.com/grafana/grafana/pkg/util" +) + +// LibraryElementService is a fake with only the required methods implemented while the others are stubbed. +type LibraryElementService struct { + elements map[string]model.LibraryElementDTO + mx sync.RWMutex + idCounter int64 +} + +var _ libraryelements.Service = (*LibraryElementService)(nil) + +func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) { + l.mx.Lock() + defer l.mx.Unlock() + + if len(l.elements) == 0 { + l.elements = make(map[string]model.LibraryElementDTO, 0) + } + + var orgID int64 = 1 + if signedInOrgID := signedInUser.GetOrgID(); signedInOrgID != 0 { + orgID = signedInOrgID + } + + var folderUID string + if cmd.FolderUID != nil { + folderUID = *cmd.FolderUID + } + + createUID := cmd.UID + if len(createUID) == 0 { + createUID = util.GenerateShortUID() + } + + if _, exists := l.elements[createUID]; exists { + return model.LibraryElementDTO{}, model.ErrLibraryElementAlreadyExists + } + + l.idCounter++ + + dto := model.LibraryElementDTO{ + ID: l.idCounter, + OrgID: orgID, + FolderID: cmd.FolderID, //nolint: staticcheck + FolderUID: folderUID, + UID: createUID, + Name: cmd.Name, + Kind: cmd.Kind, + Type: "text", + Description: "A description", + Model: cmd.Model, + Version: 1, + Meta: model.LibraryElementDTOMeta{}, + } + + l.elements[createUID] = dto + + return dto, nil +} + +func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { + l.mx.RLock() + defer l.mx.RUnlock() + + libraryElement, exists := l.elements[cmd.UID] + if !exists { + return model.LibraryElementDTO{}, model.ErrLibraryElementNotFound + } + + return libraryElement, nil +} + +func (l *LibraryElementService) GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error) { + return map[string]model.LibraryElementDTO{}, nil +} + +func (l *LibraryElementService) ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error { + return nil +} + +func (l *LibraryElementService) DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error { + return nil +} + +func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error { + return nil +} + +func (l *LibraryElementService) GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) { + elements := make([]model.LibraryElementDTO, 0, len(l.elements)) + + var orgID int64 = 1 + if signedInOrgID := signedInUser.GetOrgID(); signedInOrgID != 0 { + orgID = signedInOrgID + } + + l.mx.RLock() + defer l.mx.RUnlock() + + for _, element := range l.elements { + if element.OrgID != orgID { + continue + } + + elements = append(elements, element) + } + + // For this fake ignore pagination to make it simpler. + return model.LibraryElementSearchResult{ + TotalCount: int64(len(elements)), + Elements: elements, + Page: 1, + PerPage: len(elements), + }, nil +} diff --git a/pkg/services/libraryelements/fake/libraryelements_service_test.go b/pkg/services/libraryelements/fake/libraryelements_service_test.go new file mode 100644 index 00000000000..a246e4c6e2b --- /dev/null +++ b/pkg/services/libraryelements/fake/libraryelements_service_test.go @@ -0,0 +1,132 @@ +package fake_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/services/libraryelements/fake" + "github.com/grafana/grafana/pkg/services/libraryelements/model" +) + +func TestLibraryElementService(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + t.Run("GetElement", func(t *testing.T) { + t.Parallel() + + user := &identity.StaticRequester{} + + t.Run("when the element does not exist, it returns a `model.ErrLibraryElementNotFound` error", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + element, err := svc.GetElement(ctx, user, model.GetLibraryElementCommand{UID: "does-not-exist"}) + require.ErrorIs(t, err, model.ErrLibraryElementNotFound) + require.Empty(t, element) + }) + + t.Run("when the element exists, it returns it", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + uid := "uid-1" + + createdElement, err := svc.CreateElement(ctx, user, model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + UID: uid, + }) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + + element, err := svc.GetElement(ctx, user, model.GetLibraryElementCommand{UID: uid}) + require.NoError(t, err) + require.EqualValues(t, element, createdElement) + }) + }) + + t.Run("CreateElement", func(t *testing.T) { + t.Parallel() + + user := &identity.StaticRequester{} + + t.Run("when the uid already exists, it returns a `model.ErrLibraryElementAlreadyExists` error", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + UID: "uid-1", + } + + createdElement, err := svc.CreateElement(ctx, user, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + + createdElement, err = svc.CreateElement(ctx, user, cmd) + require.ErrorIs(t, err, model.ErrLibraryElementAlreadyExists) + require.Empty(t, createdElement) + }) + + t.Run("when the uid is not passed in, it generates a new one", func(t *testing.T) { + t.Parallel() + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + } + + createdElement, err := svc.CreateElement(ctx, user, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement) + require.NotEmpty(t, createdElement.UID) + }) + }) + + t.Run("GetAllElements", func(t *testing.T) { + t.Parallel() + + t.Run("only returns the elements belonging to the requestor org", func(t *testing.T) { + t.Parallel() + + user1 := &identity.StaticRequester{OrgID: 1} + user2 := &identity.StaticRequester{OrgID: 2} + + svc := &fake.LibraryElementService{} + + cmd := model.CreateLibraryElementCommand{ + Name: "ElementName", + Model: []byte{}, + Kind: int64(model.PanelElement), + } + + createdElement1, err := svc.CreateElement(ctx, user1, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement1) + + createdElement2, err := svc.CreateElement(ctx, user2, cmd) + require.NoError(t, err) + require.NotEmpty(t, createdElement2) + + result, err := svc.GetAllElements(ctx, user2, model.SearchLibraryElementsQuery{}) + require.NoError(t, err) + require.Len(t, result.Elements, 1) + require.Equal(t, createdElement2.UID, result.Elements[0].UID) + }) + }) +} diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index 6108e585ef0..3fbbdca72d9 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -15,7 +15,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) *LibraryElementService { +func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.RouteRegister, folderService folder.Service, folderStore folder.Store, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) *LibraryElementService { l := &LibraryElementService{ Cfg: cfg, SQLStore: sqlStore, @@ -27,7 +27,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout } l.registerAPIEndpoints() - ac.RegisterScopeAttributeResolver(LibraryPanelUIDScopeResolver(l, l.folderService)) + ac.RegisterScopeAttributeResolver(LibraryPanelUIDScopeResolver(l, folderStore)) return l } @@ -40,6 +40,7 @@ type Service interface { ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error DisconnectElementsFromDashboard(c context.Context, dashboardID int64) error DeleteLibraryElementsInFolder(c context.Context, signedInUser identity.Requester, folderUID string) error + GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) } // LibraryElementService is the service for the Library Element feature. @@ -85,6 +86,11 @@ func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, return l.deleteLibraryElementsInFolderUID(c, signedInUser, folderUID) } +// GetAll gets all library elements with support to query filters. +func (l *LibraryElementService) GetAllElements(c context.Context, signedInUser identity.Requester, query model.SearchLibraryElementsQuery) (model.LibraryElementSearchResult, error) { + return l.getAllLibraryElements(c, signedInUser, query) +} + func (l *LibraryElementService) addUidToLibraryPanel(model []byte, newUid string) (json.RawMessage, error) { var modelMap map[string]any err := json.Unmarshal(model, &modelMap) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 1f5c22168ff..94e19f86380 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -308,6 +308,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), + folder.NewFakeStore(), nil, ) require.NoError(t, err) @@ -323,12 +324,15 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder features := featuremgmt.WithFeatures() cfg := setting.NewCfg() ac := actest.FakeAccessControl{ExpectedEvaluate: true} + folderPermissions := acmock.NewMockedPermissionsService() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + store := folderimpl.ProvideStore(sc.sqlStore) + s := folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) t.Logf("Creating folder with title and UID %q", title) ctx := identity.WithRequester(context.Background(), &sc.user) folder, err := s.Create(ctx, &folder.CreateFolderCommand{ @@ -391,7 +395,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena dashboardService, svcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, ac, - foldertest.NewFakeService(), + foldertest.NewFakeService(), folder.NewFakeStore(), nil, ) require.NoError(t, svcErr) @@ -453,16 +457,19 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, ac, - foldertest.NewFakeService(), + foldertest.NewFakeService(), folder.NewFakeStore(), nil, ) require.NoError(t, dashSvcErr) guardian.InitAccessControlGuardian(cfg, ac, dashService) + fStore := folderimpl.ProvideStore(sqlStore) + folderSrv := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore, + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) service := LibraryElementService{ Cfg: cfg, features: featuremgmt.WithFeatures(), SQLStore: sqlStore, - folderService: folderimpl.ProvideService(ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()), + folderService: folderSrv, } // deliberate difference between signed in user and user in db to make it crystal clear diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 89bcb12b3cb..f61304f9837 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" @@ -732,7 +733,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash service, err := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, - foldertest.NewFakeService(), + foldertest.NewFakeService(), folder.NewFakeStore(), nil, ) require.NoError(t, err) @@ -747,12 +748,15 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder features := featuremgmt.WithFeatures() ac := actest.FakeAccessControl{ExpectedEvaluate: true} + folderPermissions := acmock.NewMockedPermissionsService() cfg := setting.NewCfg() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore), quotaService) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore) - s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + fStore := folderimpl.ProvideStore(sc.sqlStore) + s := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore, + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) t.Logf("Creating folder with title and UID %q", title) ctx := identity.WithRequester(context.Background(), sc.user) @@ -816,6 +820,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo role := org.RoleAdmin sqlStore, cfg := db.InitTestDBWithCfg(t) quotaService := quotatest.New(false, nil) + features := featuremgmt.WithFeatures() ac := actest.FakeAccessControl{ExpectedEvaluate: true} dashStore := &dashboards.FakeDashboardStore{} @@ -823,20 +828,24 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) dashPermissionService := acmock.NewMockedPermissionsService() dashService, err := dashboardservice.ProvideDashboardServiceImpl( - setting.NewCfg(), dashStore, folderStore, - featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, - foldertest.NewFakeService(), + cfg, dashStore, folderStore, + features, acmock.NewMockedPermissionsService(), dashPermissionService, ac, + foldertest.NewFakeService(), folder.NewFakeStore(), nil, ) require.NoError(t, err) - guardian.InitAccessControlGuardian(setting.NewCfg(), ac, dashService) + guardian.InitAccessControlGuardian(cfg, ac, dashService) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService) + dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore), quotaService) require.NoError(t, err) - features := featuremgmt.WithFeatures() - folderService := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + fStore := folderimpl.ProvideStore(sqlStore) - elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, featuremgmt.WithFeatures(), ac) + folderPermissions, err := testutil.ProvideFolderPermissions(features, cfg, sqlStore) + require.NoError(t, err) + folderService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + + elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, fStore, features, ac) service := LibraryPanelService{ Cfg: cfg, SQLStore: sqlStore, diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index e9fb15d950d..ec7f9ee65bf 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -195,7 +195,7 @@ func (s *ServiceImpl) addPluginToSection(c *contextmodel.ReqContext, treeRoot *n switch sectionID { case navtree.NavIDApps: treeRoot.AddSection(&navtree.NavLink{ - Text: "Apps", + Text: "More apps", Icon: "layer-group", SubTitle: "App plugins that extend the Grafana experience", Id: navtree.NavIDApps, diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index fdac09b8450..40978045b44 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -121,14 +121,14 @@ func TestAddAppLinks(t *testing.T) { }, } - t.Run("Should move apps to Apps category", func(t *testing.T) { + t.Run("Should move apps to 'More apps' category", func(t *testing.T) { treeRoot := navtree.NavTreeRoot{} err := service.addAppLinks(&treeRoot, reqCtx) require.NoError(t, err) appsNode := treeRoot.FindById(navtree.NavIDApps) require.NotNil(t, appsNode) - require.Equal(t, "Apps", appsNode.Text) + require.Equal(t, "More apps", appsNode.Text) require.Len(t, appsNode.Children, 3) require.Equal(t, testApp1.Name, appsNode.Children[0].Text) }) @@ -169,7 +169,7 @@ func TestAddAppLinks(t *testing.T) { require.Len(t, treeRoot.Children, 2) require.Equal(t, "plugin-page-test-app1", treeRoot.Children[0].Id) - // Check if it is not under the "Apps" section anymore + // Check if it is not under the "More apps" section anymore appsNode := treeRoot.FindById(navtree.NavIDApps) require.NotNil(t, appsNode) require.Len(t, appsNode.Children, 2) @@ -197,7 +197,7 @@ func TestAddAppLinks(t *testing.T) { require.Len(t, adminNode.Children, 1) require.Equal(t, "plugin-page-test-app1", adminNode.Children[0].Id) - // Check if it is not under the "Apps" section anymore + // Check if it is not under the "More apps" section anymore appsNode := treeRoot.FindById(navtree.NavIDApps) require.NotNil(t, appsNode) require.Len(t, appsNode.Children, 2) diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index d33e93f763b..47095e0c1ef 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -167,6 +167,10 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere if sec := treeRoot.FindById(navtree.NavIDCfgAccess); sec != nil && len(sec.Children) == 0 { treeRoot.RemoveSectionByID(navtree.NavIDCfgAccess) } + // double-check and remove admin menu if empty + if sec := treeRoot.FindById(navtree.NavIDCfg); sec != nil && len(sec.Children) == 0 { + treeRoot.RemoveSectionByID(navtree.NavIDCfg) + } if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagPinNavItems) { treeRoot.AddSection(&navtree.NavLink{ @@ -417,11 +421,32 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na }) } - if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingNotificationsRead), ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead))) { + contactPointsPerms := []ac.Evaluator{ + ac.EvalPermission(ac.ActionAlertingNotificationsRead), + ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead), + } + + // With the new alerting API, we have other permissions to consider. We don't want to consider these with the old + // alerting API to maintain backwards compatibility. + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingApiServer) { + contactPointsPerms = append(contactPointsPerms, + ac.EvalPermission(ac.ActionAlertingReceiversRead), + ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), + ac.EvalPermission(ac.ActionAlertingReceiversCreate), + ) + } + + if hasAccess(ac.EvalAny(contactPointsPerms...)) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{ Text: "Contact points", SubTitle: "Choose how to notify your contact points when an alert instance fires", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications", Icon: "comment-alt-share", }) + } + + if hasAccess(ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingNotificationsRead), + ac.EvalPermission(ac.ActionAlertingNotificationsExternalRead), + )) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Notification policies", SubTitle: "Determine how alerts are routed to contact points", Id: "am-routes", Url: s.cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"}) } diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index 1aa3eb8f4c5..94ce4069160 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -178,13 +178,38 @@ var ( }, } + timeIntervalsReaderRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.time-intervals:reader", + DisplayName: "Time Intervals Reader", + Description: "Read all time intervals in Grafana alerting", + Group: AlertRolesGroup, + Permissions: []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsRead}, + }, + }, + } + + timeIntervalsWriterRole = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Name: accesscontrol.FixedRolePrefix + "alerting.time-intervals:writer", + DisplayName: "Time Intervals Writer", + Description: "Create, update, and delete all time intervals in Grafana alerting", + Group: AlertRolesGroup, + Permissions: accesscontrol.ConcatPermissions(timeIntervalsReaderRole.Role.Permissions, []accesscontrol.Permission{ + {Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsWrite}, + {Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsDelete}, + }), + }, + } + notificationsReaderRole = accesscontrol.RoleRegistration{ Role: accesscontrol.RoleDTO{ Name: accesscontrol.FixedRolePrefix + "alerting.notifications:reader", DisplayName: "Notifications Reader", Description: "Read notification policies and contact points in Grafana and external providers", Group: AlertRolesGroup, - Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, templatesReaderRole.Role.Permissions, []accesscontrol.Permission{ + Permissions: accesscontrol.ConcatPermissions(receiversReaderRole.Role.Permissions, templatesReaderRole.Role.Permissions, timeIntervalsReaderRole.Role.Permissions, []accesscontrol.Permission{ { Action: accesscontrol.ActionAlertingNotificationsRead, }, @@ -192,9 +217,6 @@ var ( Action: accesscontrol.ActionAlertingNotificationsExternalRead, Scope: datasources.ScopeAll, }, - { - Action: accesscontrol.ActionAlertingNotificationsTimeIntervalsRead, - }, }), }, } @@ -205,7 +227,7 @@ var ( DisplayName: "Notifications Writer", Description: "Add, update, and delete contact points and notification policies in Grafana and external providers", Group: AlertRolesGroup, - Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, templatesWriterRole.Role.Permissions, []accesscontrol.Permission{ + Permissions: accesscontrol.ConcatPermissions(notificationsReaderRole.Role.Permissions, receiversWriterRole.Role.Permissions, templatesWriterRole.Role.Permissions, timeIntervalsWriterRole.Role.Permissions, []accesscontrol.Permission{ { Action: accesscontrol.ActionAlertingNotificationsWrite, }, @@ -337,7 +359,7 @@ func DeclareFixedRoles(service accesscontrol.Service, features featuremgmt.Featu } if features.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { - fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole) + fixedRoles = append(fixedRoles, receiversReaderRole, receiversCreatorRole, receiversWriterRole, templatesReaderRole, templatesWriterRole, timeIntervalsReaderRole, timeIntervalsWriterRole) } return service.DeclareFixedRoles(fixedRoles...) diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 0ef3a17f0f0..5057b831b08 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -63,6 +63,7 @@ type API struct { DataProxy *datasourceproxy.DataSourceProxyService MultiOrgAlertmanager *notifier.MultiOrgAlertmanager StateManager *state.Manager + Scheduler StatusReader AccessControl ac.AccessControl Policies *provisioning.NotificationPolicyService ReceiverService *notifier.ReceiverService @@ -115,7 +116,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { api.RegisterPrometheusApiEndpoints(NewForkingProm( api.DatasourceCache, NewLotexProm(proxy, logger), - &PrometheusSrv{log: logger, manager: api.StateManager, store: api.RuleStore, authz: ruleAuthzService}, + &PrometheusSrv{log: logger, manager: api.StateManager, status: api.Scheduler, store: api.RuleStore, authz: ruleAuthzService}, ), m) // Register endpoints for proxying to Cortex Ruler-compatible backends. api.RegisterRulerApiEndpoints(NewForkingRuler( diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 7953772e5b1..dfe2f01a996 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -9,7 +9,6 @@ import ( "sort" "strconv" "strings" - "time" "github.com/prometheus/alertmanager/pkg/labels" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" @@ -24,9 +23,14 @@ import ( "github.com/grafana/grafana/pkg/util" ) +type StatusReader interface { + Status(key ngmodels.AlertRuleKey) (ngmodels.RuleStatus, bool) +} + type PrometheusSrv struct { log log.Logger manager state.AlertInstanceManager + status StatusReader store RuleStore authz RuleAccessControlService } @@ -222,7 +226,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon namespaces[namespaceUID] = folder.Fullpath } - ruleResponse = PrepareRuleGroupStatuses(srv.log, srv.manager, srv.store, RuleGroupStatusesOptions{ + ruleResponse = PrepareRuleGroupStatuses(srv.log, srv.manager, srv.status, srv.store, RuleGroupStatusesOptions{ Ctx: c.Req.Context(), OrgID: c.OrgID, Query: c.Req.Form, @@ -235,7 +239,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) } -func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse { +func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, status StatusReader, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse { ruleResponse := apimodels.RuleResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -346,7 +350,7 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager continue } - ruleGroup, totals := toRuleGroup(log, manager, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) + ruleGroup, totals := toRuleGroup(log, manager, status, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) ruleGroup.Totals = totals for k, v := range totals { rulesTotals[k] += v @@ -432,7 +436,7 @@ func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool { return true } -func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) { +func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, sr StatusReader, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) { newGroup := &apimodels.RuleGroup{ Name: groupKey.RuleGroup, // file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana. @@ -443,6 +447,15 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng ngmodels.RulesGroup(rules).SortByGroupIndex() for _, rule := range rules { + status, ok := sr.Status(rule.GetKey()) + // Grafana by design return "ok" health and default other fields for unscheduled rules. + // This differs from Prometheus. + if !ok { + status = ngmodels.RuleStatus{ + Health: "ok", + } + } + alertingRule := apimodels.AlertingRule{ State: "inactive", Name: rule.Title, @@ -454,9 +467,11 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng newRule := apimodels.Rule{ Name: rule.Title, Labels: apimodels.LabelsFromMap(rule.GetLabels(labelOptions...)), - Health: "ok", + Health: status.Health, + LastError: errorOrEmpty(status.LastError), Type: rule.Type().String(), - LastEvaluation: time.Time{}, + LastEvaluation: status.EvaluationTimestamp, + EvaluationTime: status.EvaluationDuration.Seconds(), } states := manager.GetStatesForRuleUID(rule.OrgID, rule.UID) @@ -485,12 +500,6 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng Value: valString, } - if alertState.LastEvaluationTime.After(newRule.LastEvaluation) { - newRule.LastEvaluation = alertState.LastEvaluationTime - } - - newRule.EvaluationTime = alertState.EvaluationDuration.Seconds() - switch alertState.State { case eval.Normal: case eval.Pending: @@ -503,14 +512,7 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ng } alertingRule.State = "firing" case eval.Error: - newRule.Health = "error" case eval.NoData: - newRule.Health = "nodata" - } - - if alertState.Error != nil { - newRule.LastError = alertState.Error.Error() - newRule.Health = "error" } if len(withStates) > 0 { @@ -604,3 +606,10 @@ func encodedQueriesOrError(rules []ngmodels.AlertQuery) string { return err.Error() } + +func errorOrEmpty(err error) string { + if err != nil { + return err.Error() + } + return "" +} diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 478253e5646..497c3b4993f 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -489,6 +489,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { t.Run("should return sorted", func(t *testing.T) { ruleStore := fakes.NewRuleStore(t) fakeAIM := NewFakeAlertInstanceManager(t) + fakeSch := newFakeSchedulerReader(t).setupStates(fakeAIM) groupKey := ngmodels.GenerateGroupKey(orgID) gen := ngmodels.RuleGen rules := gen.With(gen.WithGroupKey(groupKey), gen.WithUniqueGroupIndex()).GenerateManyRef(5, 10) @@ -497,6 +498,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, + status: fakeSch, store: ruleStore, authz: &fakeRuleAccessControlService{}, } @@ -558,6 +560,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, + status: newFakeSchedulerReader(t).setupStates(fakeAIM), store: ruleStore, authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())), } @@ -673,6 +676,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, + status: newFakeSchedulerReader(t).setupStates(fakeAIM), store: ruleStore, authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())), } @@ -1389,11 +1393,13 @@ func TestRouteGetRuleStatuses(t *testing.T) { func setupAPI(t *testing.T) (*fakes.RuleStore, *fakeAlertInstanceManager, PrometheusSrv) { fakeStore := fakes.NewRuleStore(t) fakeAIM := NewFakeAlertInstanceManager(t) + fakeSch := newFakeSchedulerReader(t).setupStates(fakeAIM) fakeAuthz := &fakeRuleAccessControlService{} api := PrometheusSrv{ log: log.NewNopLogger(), manager: fakeAIM, + status: fakeSch, store: fakeStore, authz: fakeAuthz, } diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index 32fa6c7d460..f4e0a2e7eb5 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -27,6 +27,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -1817,11 +1818,14 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { }}, nil).Maybe() ac := &recordingAccessControlFake{} + folderPermissions := acmock.NewMockedPermissionsService() dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - folderService := folderimpl.ProvideService(actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + fStore := folderimpl.ProvideStore(sqlStore) + folderService := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore, + featuremgmt.WithFeatures(), cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) store := store.DBstore{ Logger: log, SQLStore: sqlStore, diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index 08b48544c65..e0d77de1a16 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -8,6 +8,7 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/web" ) @@ -111,7 +112,7 @@ func (api *API) authorize(method, path string) web.Handler { case http.MethodGet + "/api/ruler/{DatasourceUID}/api/v1/rules": eval = ac.EvalPermission(ac.ActionAlertingRuleExternalRead, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) case http.MethodPost + "/api/ruler/{DatasourceUID}/api/v1/rules/{Namespace}": - eval = ac.EvalPermission(ac.ActionAlertingInstancesExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) + eval = ac.EvalPermission(ac.ActionAlertingRuleExternalWrite, datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":DatasourceUID"))) // Lotex Prometheus-compatible Paths case http.MethodGet + "/api/prometheus/{DatasourceUID}/api/v1/rules": @@ -242,9 +243,8 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodGet + "/api/v1/ngalert/alertmanagers": return middleware.ReqOrgAdmin - // Grafana-only Provisioning Read Paths + // Grafana-only Provisioning Export Paths for everything except contact points. case http.MethodGet + "/api/v1/provisioning/policies/export", - http.MethodGet + "/api/v1/provisioning/contact-points/export", http.MethodGet + "/api/v1/provisioning/mute-timings/export", http.MethodGet + "/api/v1/provisioning/mute-timings/{name}/export": eval = ac.EvalAny( @@ -254,6 +254,22 @@ func (api *API) authorize(method, path string) web.Handler { ac.EvalPermission(ac.ActionAlertingProvisioningReadSecrets), // organization scope ) + // Grafana-only Provisioning Export Paths for contact points. + case http.MethodGet + "/api/v1/provisioning/contact-points/export": + perms := []ac.Evaluator{ + ac.EvalPermission(ac.ActionAlertingNotificationsRead), // organization scope + ac.EvalPermission(ac.ActionAlertingProvisioningRead), // organization scope + ac.EvalPermission(ac.ActionAlertingNotificationsProvisioningRead), // organization scope + ac.EvalPermission(ac.ActionAlertingProvisioningReadSecrets), // organization scope + } + if api.FeatureManager.IsEnabledGlobally(featuremgmt.FlagAlertingApiServer) { + perms = append(perms, + ac.EvalPermission(ac.ActionAlertingReceiversRead), + ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets), + ) + } + eval = ac.EvalAny(perms...) + case http.MethodGet + "/api/v1/provisioning/alert-rules", http.MethodGet + "/api/v1/provisioning/alert-rules/export": eval = ac.EvalAny( diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index b16305cfd78..76eaae47437 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/featuremgmt" ) func TestAuthorize(t *testing.T) { @@ -43,7 +44,7 @@ func TestAuthorize(t *testing.T) { require.Len(t, paths, 59) ac := acmock.New() - api := &API{AccessControl: ac} + api := &API{AccessControl: ac, FeatureManager: featuremgmt.WithFeatures()} t.Run("should not panic on known routes", func(t *testing.T) { for path, methods := range paths { diff --git a/pkg/services/ngalert/api/testing.go b/pkg/services/ngalert/api/testing.go index cb9dba0a02d..c219a96610e 100644 --- a/pkg/services/ngalert/api/testing.go +++ b/pkg/services/ngalert/api/testing.go @@ -166,3 +166,29 @@ func (f fakeRuleAccessControlService) AuthorizeDatasourceAccessForRule(ctx conte func (f fakeRuleAccessControlService) AuthorizeDatasourceAccessForRuleGroup(ctx context.Context, user identity.Requester, rules models.RulesGroup) error { return nil } + +type statesReader interface { + GetStatesForRuleUID(orgID int64, alertRuleUID string) []*state.State +} + +type fakeSchedulerReader struct { + states statesReader +} + +func newFakeSchedulerReader(t *testing.T) *fakeSchedulerReader { + return &fakeSchedulerReader{} +} + +// setupStates allows the fake scheduler to return data consistent with states defined elsewhere. +// This can be combined with fakeAlertInstanceManager, for instance. +func (f *fakeSchedulerReader) setupStates(reader statesReader) *fakeSchedulerReader { + f.states = reader + return f +} + +func (f *fakeSchedulerReader) Status(key models.AlertRuleKey) (models.RuleStatus, bool) { + if f.states == nil { + return models.RuleStatus{}, false + } + return state.StatesToRuleStatus(f.states.GetStatesForRuleUID(key.OrgID, key.UID)), true +} diff --git a/pkg/services/ngalert/api/tooling/api.json b/pkg/services/ngalert/api/tooling/api.json index 6539d39c71e..965cee8114f 100644 --- a/pkg/services/ngalert/api/tooling/api.json +++ b/pkg/services/ngalert/api/tooling/api.json @@ -163,6 +163,14 @@ ], "type": "object" }, + "AlertRuleEditorSettings": { + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + }, + "type": "object" + }, "AlertRuleExport": { "properties": { "annotations": { @@ -286,6 +294,14 @@ }, "type": "object" }, + "AlertRuleMetadata": { + "properties": { + "editor_settings": { + "$ref": "#/definitions/AlertRuleEditorSettings" + } + }, + "type": "object" + }, "AlertRuleNotificationSettings": { "properties": { "group_by": { @@ -1573,6 +1589,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -1660,12 +1679,25 @@ }, "GettableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/GettableExtendedRuleNode" @@ -2786,6 +2818,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "no_data_state": { "enum": [ "Alerting", @@ -2824,17 +2859,36 @@ }, "PostableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/PostableExtendedRuleNode" }, "type": "array" + }, + "source_tenants": { + "items": { + "type": "string" + }, + "type": "array" } }, "type": "object" @@ -3616,12 +3670,25 @@ }, "RuleGroupConfigResponse": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/GettableExtendedRuleNode" diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index 044d74c3ba4..304a4edcae9 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -2,7 +2,6 @@ package definitions import ( "context" - "encoding/base64" "encoding/json" "fmt" "time" @@ -680,19 +679,11 @@ func (c *PostableUserConfig) Decrypt(decryptFn func(payload []byte) ([]byte, err // Iterate through receivers and decrypt secure settings. for _, rcv := range newCfg.AlertmanagerConfig.Receivers { for _, gmr := range rcv.PostableGrafanaReceivers.GrafanaManagedReceivers { - for k, v := range gmr.SecureSettings { - decoded, err := base64.StdEncoding.DecodeString(v) - if err != nil { - return PostableUserConfig{}, fmt.Errorf("failed to decode value for key '%s': %w", k, err) - } - - decrypted, err := decryptFn(decoded) - if err != nil { - return PostableUserConfig{}, fmt.Errorf("failed to decrypt value for key '%s': %w", k, err) - } - - gmr.SecureSettings[k] = string(decrypted) + decrypted, err := gmr.DecryptSecureSettings(decryptFn) + if err != nil { + return PostableUserConfig{}, err } + gmr.SecureSettings = decrypted } } return *newCfg, nil diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index cf4c693ad69..bca1323a41b 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -237,6 +237,14 @@ type PostableRuleGroupConfig struct { Name string `yaml:"name" json:"name"` Interval model.Duration `yaml:"interval,omitempty" json:"interval,omitempty"` Rules []PostableExtendedRuleNode `yaml:"rules" json:"rules"` + + // fields below are used by Mimir/Loki rulers + + SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"` + EvaluationDelay *model.Duration `yaml:"evaluation_delay,omitempty" json:"evaluation_delay,omitempty"` + QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"` + AlignEvaluationTimeOnInterval bool `yaml:"align_evaluation_time_on_interval,omitempty" json:"align_evaluation_time_on_interval,omitempty"` + Limit int `yaml:"limit,omitempty" json:"limit,omitempty"` } func (c *PostableRuleGroupConfig) UnmarshalJSON(b []byte) error { @@ -275,15 +283,26 @@ func (c *PostableRuleGroupConfig) validate() error { if hasGrafRules && hasLotexRules { return fmt.Errorf("cannot mix Grafana & Prometheus style rules") } + + if hasGrafRules && (len(c.SourceTenants) > 0 || c.EvaluationDelay != nil || c.QueryOffset != nil || c.AlignEvaluationTimeOnInterval || c.Limit > 0) { + return fmt.Errorf("fields source_tenants, evaluation_delay, query_offset, align_evaluation_time_on_interval and limit are not supported for Grafana rules") + } return nil } // swagger:model type GettableRuleGroupConfig struct { - Name string `yaml:"name" json:"name"` - Interval model.Duration `yaml:"interval,omitempty" json:"interval,omitempty"` - SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"` - Rules []GettableExtendedRuleNode `yaml:"rules" json:"rules"` + Name string `yaml:"name" json:"name"` + Interval model.Duration `yaml:"interval,omitempty" json:"interval,omitempty"` + Rules []GettableExtendedRuleNode `yaml:"rules" json:"rules"` + + // fields below are used by Mimir/Loki rulers + + SourceTenants []string `yaml:"source_tenants,omitempty" json:"source_tenants,omitempty"` + EvaluationDelay *model.Duration `yaml:"evaluation_delay,omitempty" json:"evaluation_delay,omitempty"` + QueryOffset *model.Duration `yaml:"query_offset,omitempty" json:"query_offset,omitempty"` + AlignEvaluationTimeOnInterval bool `yaml:"align_evaluation_time_on_interval,omitempty" json:"align_evaluation_time_on_interval,omitempty"` + Limit int `yaml:"limit,omitempty" json:"limit,omitempty"` } func (c *GettableRuleGroupConfig) UnmarshalJSON(b []byte) error { diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 4371f639744..eae46145ba8 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -163,6 +163,14 @@ ], "type": "object" }, + "AlertRuleEditorSettings": { + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + }, + "type": "object" + }, "AlertRuleExport": { "properties": { "annotations": { @@ -286,6 +294,14 @@ }, "type": "object" }, + "AlertRuleMetadata": { + "properties": { + "editor_settings": { + "$ref": "#/definitions/AlertRuleEditorSettings" + } + }, + "type": "object" + }, "AlertRuleNotificationSettings": { "properties": { "group_by": { @@ -1573,6 +1589,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -1660,12 +1679,25 @@ }, "GettableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/GettableExtendedRuleNode" @@ -2786,6 +2818,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "no_data_state": { "enum": [ "Alerting", @@ -2824,17 +2859,36 @@ }, "PostableRuleGroupConfig": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/PostableExtendedRuleNode" }, "type": "array" + }, + "source_tenants": { + "items": { + "type": "string" + }, + "type": "array" } }, "type": "object" @@ -3616,12 +3670,25 @@ }, "RuleGroupConfigResponse": { "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "format": "int64", + "type": "integer" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "items": { "$ref": "#/definitions/GettableExtendedRuleNode" diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 61e5ec742aa..95716ea1899 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -3794,6 +3794,14 @@ } } }, + "AlertRuleEditorSettings": { + "type": "object", + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + } + }, "AlertRuleExport": { "type": "object", "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", @@ -3917,6 +3925,14 @@ } } }, + "AlertRuleMetadata": { + "type": "object", + "properties": { + "editor_settings": { + "$ref": "#/definitions/AlertRuleEditorSettings" + } + } + }, "AlertRuleNotificationSettings": { "type": "object", "required": [ @@ -5206,6 +5222,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -5293,12 +5312,25 @@ "GettableRuleGroupConfig": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { @@ -6420,6 +6452,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "no_data_state": { "type": "string", "enum": [ @@ -6458,17 +6493,36 @@ "PostableRuleGroupConfig": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { "$ref": "#/definitions/PostableExtendedRuleNode" } + }, + "source_tenants": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -7250,12 +7304,25 @@ "RuleGroupConfigResponse": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { diff --git a/pkg/services/ngalert/metrics/alertmanager.go b/pkg/services/ngalert/metrics/alertmanager.go index dca5c187e32..1386ba5f3b7 100644 --- a/pkg/services/ngalert/metrics/alertmanager.go +++ b/pkg/services/ngalert/metrics/alertmanager.go @@ -20,7 +20,7 @@ func NewAlertmanagerMetrics(r prometheus.Registerer, l log.Logger) *Alertmanager other := prometheus.WrapRegistererWithPrefix(fmt.Sprintf("%s_%s_", Namespace, Subsystem), r) return &Alertmanager{ Registerer: r, - Alerts: metrics.NewAlerts(other), + Alerts: metrics.NewAlerts(other, l), AlertmanagerConfigMetrics: NewAlertmanagerConfigMetrics(r, l), } } diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index bec7137c4b0..84cf3f4d4b3 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -886,3 +886,11 @@ func (r *Record) Fingerprint() data.Fingerprint { func hasAnyCondition(rule *AlertRuleWithOptionals) bool { return rule.Condition != "" || (rule.Record != nil && rule.Record.From != "") } + +// RuleStatus contains info about a rule's current evaluation state. +type RuleStatus struct { + Health string + LastError error + EvaluationTimestamp time.Time + EvaluationDuration time.Duration +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index b2628d6ae08..ab2c1f4d9b7 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -478,6 +478,7 @@ func (ng *AlertNG) init() error { ProvenanceStore: ng.store, MultiOrgAlertmanager: ng.MultiOrgAlertmanager, StateManager: ng.stateManager, + Scheduler: scheduler, AccessControl: ng.accesscontrol, Policies: policyService, ReceiverService: receiverService, diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index ad9e74cd811..264d8da6a60 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -135,7 +135,7 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A } l := log.New("ngalert.notifier.alertmanager", "org", orgID) - gam, err := alertingNotify.NewGrafanaAlertmanager("orgID", orgID, amcfg, peer, l, alertingNotify.NewGrafanaAlertmanagerMetrics(m.Registerer)) + gam, err := alertingNotify.NewGrafanaAlertmanager("orgID", orgID, amcfg, peer, l, alertingNotify.NewGrafanaAlertmanagerMetrics(m.Registerer, l)) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/notifier/redis_channel_test.go b/pkg/services/ngalert/notifier/redis_channel_test.go index 62353e39b09..5c53039c5e8 100644 --- a/pkg/services/ngalert/notifier/redis_channel_test.go +++ b/pkg/services/ngalert/notifier/redis_channel_test.go @@ -29,6 +29,8 @@ func TestNewRedisChannel(t *testing.T) { } func TestBroadcastAndHandleMessages(t *testing.T) { + t.Skip() // TODO fix the flaky test https://github.com/grafana/grafana/issues/94037 + const channelName = "testChannel" mr, err := miniredis.Run() diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index e5e819ef45f..432dc0e1b31 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -1542,14 +1542,15 @@ func TestDeleteRuleGroup(t *testing.T) { func TestProvisiongWithFullpath(t *testing.T) { tracer := tracing.InitializeTracerForTest() inProcBus := bus.ProvideBus(tracer) - sqlStore := db.InitTestDB(t) - cfg := setting.NewCfg() + sqlStore, cfg := db.InitTestDBWithCfg(t) folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) _, dashboardStore := testutil.SetupDashboardService(t, sqlStore, folderStore, cfg) ac := acmock.New() + folderPermissions := acmock.NewMockedPermissionsService() features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders) - folderService := folderimpl.ProvideService(ac, inProcBus, dashboardStore, folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - + fStore := folderimpl.ProvideStore(sqlStore) + folderService := folderimpl.ProvideService(fStore, ac, inProcBus, dashboardStore, folderStore, sqlStore, + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) ruleService := createAlertRuleService(t, folderService) var orgID int64 = 1 diff --git a/pkg/services/ngalert/provisioning/templates.go b/pkg/services/ngalert/provisioning/templates.go index 29bc8155e71..9d4db1ed140 100644 --- a/pkg/services/ngalert/provisioning/templates.go +++ b/pkg/services/ngalert/provisioning/templates.go @@ -5,6 +5,9 @@ import ( "errors" "fmt" "hash/fnv" + "maps" + "slices" + "sort" "unsafe" "github.com/grafana/grafana/pkg/infra/log" @@ -48,12 +51,15 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi } templates := make([]definitions.NotificationTemplate, 0, len(revision.Config.TemplateFiles)) - for name, tmpl := range revision.Config.TemplateFiles { + names := slices.Collect(maps.Keys(revision.Config.TemplateFiles)) + sort.Strings(names) + for _, name := range names { + content := revision.Config.TemplateFiles[name] tmpl := definitions.NotificationTemplate{ UID: legacy_storage.NameToUid(name), Name: name, - Template: tmpl, - ResourceVersion: calculateTemplateFingerprint(tmpl), + Template: content, + ResourceVersion: calculateTemplateFingerprint(content), } provenance, ok := provenances[tmpl.ResourceID()] if !ok { diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go index 050777e7691..5b989dd678d 100644 --- a/pkg/services/ngalert/provisioning/templates_test.go +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -68,7 +68,7 @@ func TestGetTemplates(t *testing.T) { }, } - require.ElementsMatch(t, expected, result) + require.EqualValues(t, expected, result) prov.AssertCalled(t, "GetProvenances", mock.Anything, orgID, (&definitions.NotificationTemplate{}).ResourceType()) prov.AssertExpectations(t) diff --git a/pkg/services/ngalert/remote/alertmanager.go b/pkg/services/ngalert/remote/alertmanager.go index 48350b02ff1..36c6e631b67 100644 --- a/pkg/services/ngalert/remote/alertmanager.go +++ b/pkg/services/ngalert/remote/alertmanager.go @@ -527,17 +527,26 @@ func (am *Alertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver, } func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) { + fn := func(payload []byte) ([]byte, error) { + return am.decrypt(ctx, payload) + } + receivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) for _, r := range c.Receivers { integrations := make([]*alertingNotify.GrafanaIntegrationConfig, 0, len(r.GrafanaManagedReceivers)) for _, gr := range r.PostableGrafanaReceivers.GrafanaManagedReceivers { + decrypted, err := gr.DecryptSecureSettings(fn) + if err != nil { + return nil, 0, err + } + integrations = append(integrations, &alertingNotify.GrafanaIntegrationConfig{ UID: gr.UID, Name: gr.Name, Type: gr.Type, DisableResolveMessage: gr.DisableResolveMessage, Settings: json.RawMessage(gr.Settings), - SecureSettings: gr.SecureSettings, + SecureSettings: decrypted, }) } receivers = append(receivers, &alertingNotify.APIReceiver{ diff --git a/pkg/services/ngalert/remote/alertmanager_test.go b/pkg/services/ngalert/remote/alertmanager_test.go index 5d6b9644e34..5e95752ea0e 100644 --- a/pkg/services/ngalert/remote/alertmanager_test.go +++ b/pkg/services/ngalert/remote/alertmanager_test.go @@ -335,6 +335,65 @@ func TestCompareAndSendConfiguration(t *testing.T) { } } +func Test_TestReceiversDecryptsSecureSettings(t *testing.T) { + const testKey = "test-key" + const testValue = "test-value" + decryptFn := func(_ context.Context, payload []byte) ([]byte, error) { + if string(payload) == testValue { + return []byte(testValue), nil + } + return nil, errTest + } + + var got apimodels.TestReceiversConfigBodyParams + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + require.NoError(t, json.NewDecoder(r.Body).Decode(&got)) + require.NoError(t, r.Body.Close()) + _, err := w.Write([]byte(`{"status": "success"}`)) + require.NoError(t, err) + })) + + fstore := notifier.NewFileStore(1, ngfakes.NewFakeKVStore(t)) + m := metrics.NewRemoteAlertmanagerMetrics(prometheus.NewRegistry()) + cfg := AlertmanagerConfig{ + OrgID: 1, + TenantID: "test", + URL: server.URL, + DefaultConfig: defaultGrafanaConfig, + } + + am, err := NewAlertmanager(cfg, + fstore, + decryptFn, + NoopAutogenFn, + m, + tracing.InitializeTracerForTest(), + ) + + require.NoError(t, err) + params := apimodels.TestReceiversConfigBodyParams{ + Alert: &apimodels.TestReceiversConfigAlertParams{}, + Receivers: []*definition.PostableApiReceiver{ + { + PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{ + { + SecureSettings: map[string]string{ + testKey: base64.StdEncoding.EncodeToString([]byte(testValue)), + }, + }, + }, + }, + }, + }, + } + + _, _, err = am.TestReceivers(context.Background(), params) + require.NoError(t, err) + require.Equal(t, map[string]string{testKey: testValue}, got.Receivers[0].PostableGrafanaReceivers.GrafanaManagedReceivers[0].SecureSettings) +} + func Test_isDefaultConfiguration(t *testing.T) { parsedDefaultConfig, _ := notifier.Load([]byte(defaultGrafanaConfig)) parsedTestConfig, _ := notifier.Load([]byte(testGrafanaConfig)) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index aa3218e7ad8..25ceaf47783 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -40,6 +40,8 @@ type Rule interface { Update(lastVersion RuleVersionAndPauseStatus) bool // Type gives the type of the rule. Type() ngmodels.RuleType + // Status indicates the status of the evaluating rule. + Status() ngmodels.RuleStatus } type ruleFactoryFunc func(context.Context, *ngmodels.AlertRule) Rule @@ -180,6 +182,10 @@ func (a *alertRule) Type() ngmodels.RuleType { return ngmodels.RuleTypeAlerting } +func (a *alertRule) Status() ngmodels.RuleStatus { + return a.stateManager.GetStatusForRuleUID(a.key.OrgID, a.key.UID) +} + // eval signals the rule evaluation routine to perform the evaluation of the rule. Does nothing if the loop is stopped. // Before sending a message into the channel, it does non-blocking read to make sure that there is no concurrent send operation. // Returns a tuple where first element is diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index d88637ddd65..0621204481d 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -369,6 +369,17 @@ func TestRuleRoutine(t *testing.T) { require.Equal(t, s.Labels, data.Labels(cmd.Labels)) }) + t.Run("status should accurately reflect latest evaluation", func(t *testing.T) { + states := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) + require.NotEmpty(t, states) + + status := ruleInfo.Status() + require.Equal(t, "ok", status.Health) + require.Nil(t, status.LastError) + require.Equal(t, states[0].LastEvaluationTime, status.EvaluationTimestamp) + require.Equal(t, states[0].EvaluationDuration, status.EvaluationDuration) + }) + t.Run("it reports metrics", func(t *testing.T) { // duration metric has 0 values because of mocked clock that do not advance expectedMetric := fmt.Sprintf( @@ -700,6 +711,15 @@ func TestRuleRoutine(t *testing.T) { assert.Len(t, args.PostableAlerts, 1) assert.Equal(t, state.ErrorAlertName, args.PostableAlerts[0].Labels[prometheusModel.AlertNameLabel]) }) + + t.Run("status should reflect unhealthy rule", func(t *testing.T) { + status := ruleInfo.Status() + require.Equal(t, "error", status.Health) + require.NotNil(t, status.LastError, "expected status to carry the latest evaluation error") + require.Contains(t, status.LastError.Error(), "cannot reference itself") + require.Equal(t, int64(0), status.EvaluationTimestamp.UTC().Unix()) + require.Equal(t, time.Duration(0), status.EvaluationDuration) + }) }) t.Run("when there are alerts that should be firing", func(t *testing.T) { diff --git a/pkg/services/ngalert/schedule/recording_rule.go b/pkg/services/ngalert/schedule/recording_rule.go index 4bcc5868f38..3abf44b6457 100644 --- a/pkg/services/ngalert/schedule/recording_rule.go +++ b/pkg/services/ngalert/schedule/recording_rule.go @@ -84,8 +84,8 @@ func (r *recordingRule) Type() ngmodels.RuleType { return ngmodels.RuleTypeRecording } -func (r *recordingRule) Status() RuleStatus { - return RuleStatus{ +func (r *recordingRule) Status() ngmodels.RuleStatus { + return ngmodels.RuleStatus{ Health: r.health.Load(), LastError: r.lastError.Load(), EvaluationTimestamp: r.evaluationTimestamp.Load(), diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index 157124373f5..2b162ca87be 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -56,6 +56,14 @@ func (r *ruleRegistry) exists(key models.AlertRuleKey) bool { return ok } +// get fetches a rule from the registry by key. It returns (rule, ok) where ok is false if the rule did not exist. +func (r *ruleRegistry) get(key models.AlertRuleKey) (Rule, bool) { + r.mu.Lock() + defer r.mu.Unlock() + ru, ok := r.rules[key] + return ru, ok +} + // del removes pair that has specific key from the registry. // Returns 2-tuple where the first element is value of the removed pair // and the second element indicates whether element with the specified key existed. diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index bd33a45ff1d..d05de8a74b0 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -171,6 +171,14 @@ func (sch *schedule) Rules() ([]*ngmodels.AlertRule, map[ngmodels.FolderKey]stri return sch.schedulableAlertRules.all() } +// Status fetches the health of a given scheduled rule, by key. +func (sch *schedule) Status(key ngmodels.AlertRuleKey) (ngmodels.RuleStatus, bool) { + if rule, ok := sch.registry.get(key); ok { + return rule.Status(), true + } + return ngmodels.RuleStatus{}, false +} + // deleteAlertRule stops evaluation of the rule, deletes it from active rules, and cleans up state cache. func (sch *schedule) deleteAlertRule(keys ...ngmodels.AlertRuleKey) { for _, key := range keys { diff --git a/pkg/services/ngalert/schedule/schedule_unit_test.go b/pkg/services/ngalert/schedule/schedule_unit_test.go index 54b83cfb277..c75900a4d63 100644 --- a/pkg/services/ngalert/schedule/schedule_unit_test.go +++ b/pkg/services/ngalert/schedule/schedule_unit_test.go @@ -113,6 +113,11 @@ func TestProcessTicks(t *testing.T) { folderWithRuleGroup1 := fmt.Sprintf("%s;%s", ruleStore.getNamespaceTitle(alertRule1.NamespaceUID), alertRule1.RuleGroup) + t.Run("before 1st tick status should not be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.False(t, ok, "status for a rule should not be present before the scheduler has created it") + }) + t.Run("on 1st tick alert rule should be evaluated", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) @@ -137,12 +142,25 @@ func TestProcessTicks(t *testing.T) { require.NoError(t, err) }) + t.Run("after 1st tick status for rule should be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + // Interestingly, the rules in this test are randomised, and are sometimes invalid. + // Therefore, we can't reliably assert anything about the actual health. It might be error, it might not, depending on randomness. + // We are only testing that things were scheduled, not that the rule routine worked internally. + }) + // add alert rule under main org with three base intervals alertRule2 := gen.With(gen.WithOrgID(mainOrgID), gen.WithInterval(3*cfg.BaseInterval), gen.WithTitle("rule-2")).GenerateRef() ruleStore.PutRule(ctx, alertRule2) folderWithRuleGroup2 := fmt.Sprintf("%s;%s", ruleStore.getNamespaceTitle(alertRule2.NamespaceUID), alertRule2.RuleGroup) + t.Run("before 2nd tick status for rule should not be available", func(t *testing.T) { + _, ok := sched.Status(alertRule2.GetKey()) + require.False(t, ok, "status for a rule should not be present before the scheduler has created it") + }) + t.Run("on 2nd tick first alert rule should be evaluated", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick) @@ -184,6 +202,16 @@ func TestProcessTicks(t *testing.T) { assertEvalRun(t, evalAppliedCh, tick, keys...) }) + t.Run("after 3rd tick status for both rules should be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + _, ok = sched.Status(alertRule2.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + // Interestingly, the rules in this test are randomised, and are sometimes invalid. + // Therefore, we can't reliably assert anything about the actual health. It might be error, it might not, depending on randomness. + // We are only testing that things were scheduled, not that the rule routine worked internally. + }) + t.Run("on 4th tick only one alert rule should be evaluated", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick) @@ -223,6 +251,16 @@ func TestProcessTicks(t *testing.T) { require.NoError(t, err) }) + t.Run("after 5th tick status for both rules should be available regardless of pause state", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + _, ok = sched.Status(alertRule2.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + // Interestingly, the rules in this test are randomised, and are sometimes invalid. + // Therefore, we can't reliably assert anything about the actual health. It might be error, it might not, depending on randomness. + // We are only testing that things were scheduled, not that the rule routine worked internally. + }) + t.Run("on 6th tick all alert rule are paused (it still enters evaluation but it is early skipped)", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) @@ -309,6 +347,13 @@ func TestProcessTicks(t *testing.T) { require.NoError(t, err) }) + t.Run("after 8th tick status for deleted rule should not be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.False(t, ok, "status for a rule that was deleted should not be available") + _, ok = sched.Status(alertRule2.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + }) + t.Run("on 9th tick one alert rule should be evaluated", func(t *testing.T) { tick = tick.Add(cfg.BaseInterval) @@ -338,6 +383,14 @@ func TestProcessTicks(t *testing.T) { require.Emptyf(t, updated, "None rules are expected to be updated") assertEvalRun(t, evalAppliedCh, tick, alertRule3.GetKey()) }) + t.Run("after 10th tick status for remaining rules should be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.False(t, ok, "status for a rule that was deleted should not be available") + _, ok = sched.Status(alertRule2.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + _, ok = sched.Status(alertRule3.GetKey()) + require.True(t, ok, "status for a rule that just evaluated was not available") + }) t.Run("on 11th tick rule2 should be updated", func(t *testing.T) { newRule2 := models.CopyRule(alertRule2) newRule2.Version++ @@ -465,6 +518,14 @@ func TestProcessTicks(t *testing.T) { require.Emptyf(t, updated, "No rules should be updated") }) + t.Run("after 12th tick no status should be available", func(t *testing.T) { + _, ok := sched.Status(alertRule1.GetKey()) + require.False(t, ok, "status for a rule that was deleted should not be available") + _, ok = sched.Status(alertRule2.GetKey()) + require.False(t, ok, "status for a rule that just evaluated was not available") + _, ok = sched.Status(alertRule3.GetKey()) + require.False(t, ok, "status for a rule that just evaluated was not available") + }) t.Run("scheduled rules should be sorted", func(t *testing.T) { rules := gen.With(gen.WithOrgID(mainOrgID), gen.WithInterval(cfg.BaseInterval)).GenerateManyRef(10, 20) diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index 14c7f97b251..d2353ec42d8 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -99,7 +99,7 @@ func (h *AnnotationBackend) Record(ctx context.Context, rule history_model.RuleM err := h.store.Save(ctx, panel, annotations, rule.OrgID, logger) if err != nil { - logger.Error("Failed to save history batch", len(annotations), "err", err) + logger.Error("Failed to save history batch", "samples", len(annotations), "err", err) errCh <- err return } diff --git a/pkg/services/ngalert/state/manager.go b/pkg/services/ngalert/state/manager.go index cbea45a5db3..fc23cd85998 100644 --- a/pkg/services/ngalert/state/manager.go +++ b/pkg/services/ngalert/state/manager.go @@ -556,6 +556,11 @@ func (st *Manager) GetStatesForRuleUID(orgID int64, alertRuleUID string) []*Stat return st.cache.getStatesForRuleUID(orgID, alertRuleUID, st.doNotSaveNormalState) } +func (st *Manager) GetStatusForRuleUID(orgID int64, alertRuleUID string) ngModels.RuleStatus { + states := st.GetStatesForRuleUID(orgID, alertRuleUID) + return StatesToRuleStatus(states) +} + func (st *Manager) Put(states []*State) { for _, s := range states { st.cache.set(s) @@ -623,3 +628,34 @@ func (st *Manager) deleteStaleStatesFromCache(ctx context.Context, logger log.Lo func stateIsStale(evaluatedAt time.Time, lastEval time.Time, intervalSeconds int64) bool { return !lastEval.Add(2 * time.Duration(intervalSeconds) * time.Second).After(evaluatedAt) } + +func StatesToRuleStatus(states []*State) ngModels.RuleStatus { + status := ngModels.RuleStatus{ + Health: "ok", + LastError: nil, + EvaluationTimestamp: time.Time{}, + } + for _, state := range states { + if state.LastEvaluationTime.After(status.EvaluationTimestamp) { + status.EvaluationTimestamp = state.LastEvaluationTime + } + + status.EvaluationDuration = state.EvaluationDuration + + switch state.State { + case eval.Normal: + case eval.Pending: + case eval.Alerting: + case eval.Error: + status.Health = "error" + case eval.NoData: + status.Health = "nodata" + } + + if state.Error != nil { + status.LastError = state.Error + status.Health = "error" + } + } + return status +} diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index 6311a9ffdbb..417680c39c2 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -27,7 +27,10 @@ import ( func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStore dashboards.Store, folderStore *folderimpl.DashboardFolderStoreImpl, bus *bus.InProcBus, features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl) folder.Service { tb.Helper() - return folderimpl.ProvideService(ac, bus, dashboardStore, folderStore, db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + folderPermissions := acmock.NewMockedPermissionsService() + fStore := folderimpl.ProvideStore(db) + return folderimpl.ProvideService(fStore, ac, bus, dashboardStore, folderStore, db, + features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) } func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) { @@ -57,7 +60,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa dashboardService, err := dashboardservice.ProvideDashboardServiceImpl( cfg, dashboardStore, fs, features, folderPermissions, dashboardPermissions, ac, - foldertest.NewFakeService(), + foldertest.NewFakeService(), folder.NewFakeStore(), nil, ) require.NoError(tb, err) diff --git a/pkg/services/ngalert/writer/prom.go b/pkg/services/ngalert/writer/prom.go index 654eca9bf93..ea46b816125 100644 --- a/pkg/services/ngalert/writer/prom.go +++ b/pkg/services/ngalert/writer/prom.go @@ -66,9 +66,15 @@ func PointsFromFrames(name string, t time.Time, frames data.Frames, extraLabels points := make([]Point, 0, len(col.Refs)) for _, ref := range col.Refs { - fp, empty, _ := ref.NullableFloat64Value() - if empty || fp == nil { - return nil, fmt.Errorf("unable to read float64 value") + fp, empty, err := ref.NullableFloat64Value() + if err != nil { + return nil, fmt.Errorf("unable to read float64 value: %w", err) + } + if empty { + return nil, fmt.Errorf("empty frame") + } + if fp == nil { + return nil, fmt.Errorf("nil frame") } metric := Metric{ diff --git a/pkg/services/pluginsintegration/clientmiddleware/base_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/base_middleware.go deleted file mode 100644 index 32a795a5477..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/base_middleware.go +++ /dev/null @@ -1,60 +0,0 @@ -package clientmiddleware - -import ( - "context" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/plugins" -) - -var _ = plugins.Client(&baseMiddleware{}) - -// The base middleware simply passes the request down the chain -// This allows middleware to avoid implementing all the noop functions -type baseMiddleware struct { - next plugins.Client -} - -func (m *baseMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - return m.next.QueryData(ctx, req) -} - -func (m *baseMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - return m.next.CallResource(ctx, req, sender) -} - -func (m *baseMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - return m.next.CheckHealth(ctx, req) -} - -func (m *baseMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { - return m.next.CollectMetrics(ctx, req) -} - -func (m *baseMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { - return m.next.SubscribeStream(ctx, req) -} - -func (m *baseMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { - return m.next.PublishStream(ctx, req) -} - -func (m *baseMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { - return m.next.RunStream(ctx, req, sender) -} - -// ValidateAdmission implements backend.AdmissionHandler. -func (m *baseMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { - return m.next.ValidateAdmission(ctx, req) -} - -// MutateAdmission implements backend.AdmissionHandler. -func (m *baseMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { - return m.next.MutateAdmission(ctx, req) -} - -// ConvertObject implements backend.AdmissionHandler. -func (m *baseMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { - return m.next.ConvertObjects(ctx, req) -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go index 95756b169af..80058c0a5e8 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware.go @@ -10,7 +10,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -19,15 +18,15 @@ import ( // needed to mock the function for testing var shouldCacheQuery = awsds.ShouldCacheQuery -// NewCachingMiddleware creates a new plugins.ClientMiddleware that will +// NewCachingMiddleware creates a new backend.HandlerMiddleware that will // attempt to read and write query results to the cache -func NewCachingMiddleware(cachingService caching.CachingService) plugins.ClientMiddleware { +func NewCachingMiddleware(cachingService caching.CachingService) backend.HandlerMiddleware { return NewCachingMiddlewareWithFeatureManager(cachingService, nil) } -// NewCachingMiddlewareWithFeatureManager creates a new plugins.ClientMiddleware that will +// NewCachingMiddlewareWithFeatureManager creates a new backend.HandlerMiddleware that will // attempt to read and write query results to the cache with a feature manager -func NewCachingMiddlewareWithFeatureManager(cachingService caching.CachingService, features featuremgmt.FeatureToggles) plugins.ClientMiddleware { +func NewCachingMiddlewareWithFeatureManager(cachingService caching.CachingService, features featuremgmt.FeatureToggles) backend.HandlerMiddleware { log := log.New("caching_middleware") if err := prometheus.Register(QueryCachingRequestHistogram); err != nil { log.Error("Error registering prometheus collector 'QueryRequestHistogram'", "error", err) @@ -35,20 +34,18 @@ func NewCachingMiddlewareWithFeatureManager(cachingService caching.CachingServic if err := prometheus.Register(ResourceCachingRequestHistogram); err != nil { log.Error("Error registering prometheus collector 'ResourceRequestHistogram'", "error", err) } - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &CachingMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, - caching: cachingService, - log: log, - features: features, + BaseHandler: backend.NewBaseHandler(next), + caching: cachingService, + log: log, + features: features, } }) } type CachingMiddleware struct { - baseMiddleware + backend.BaseHandler caching caching.CachingService log log.Logger @@ -60,12 +57,12 @@ type CachingMiddleware struct { // If the cache service is implemented, we capture the request duration as a metric. The service is expected to write any response headers. func (m *CachingMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } reqCtx := contexthandler.FromContext(ctx) if reqCtx == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } // time how long this request takes @@ -92,7 +89,7 @@ func (m *CachingMiddleware) QueryData(ctx context.Context, req *backend.QueryDat } // Cache miss; do the actual queries - resp, err := m.next.QueryData(ctx, req) + resp, err := m.BaseHandler.QueryData(ctx, req) // Update the query cache with the result for this metrics request if err == nil && cr.UpdateCacheFn != nil { @@ -125,12 +122,12 @@ func (m *CachingMiddleware) QueryData(ctx context.Context, req *backend.QueryDat // If the cache service is implemented, we capture the request duration as a metric. The service is expected to write any response headers. func (m *CachingMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } reqCtx := contexthandler.FromContext(ctx) if reqCtx == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } // time how long this request takes @@ -157,7 +154,7 @@ func (m *CachingMiddleware) CallResource(ctx context.Context, req *backend.CallR // Cache miss; do the actual request // If there is no update cache func, just pass in the original sender if cr.UpdateCacheFn == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } // Otherwise, intercept the responses in a wrapped sender so we can cache them first cacheSender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { @@ -165,5 +162,5 @@ func (m *CachingMiddleware) CallResource(ctx context.Context, req *backend.CallR return sender.Send(res) }) - return m.next.CallResource(ctx, req, cacheSender) + return m.BaseHandler.CallResource(ctx, req, cacheSender) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go index 5a54eafc775..548f7a693bf 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/caching_middleware_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -22,9 +22,9 @@ func TestCachingMiddleware(t *testing.T) { require.NoError(t, err) cs := caching.NewFakeOSSCachingService() - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCachingMiddleware(cs)), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCachingMiddleware(cs)), ) jsonDataMap := map[string]any{} @@ -63,7 +63,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = true cs.ReturnQueryResponse = dataResponse - resp, err := cdt.Decorator.QueryData(req.Context(), qdr) + resp, err := cdt.MiddlewareHandler.QueryData(req.Context(), qdr) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleQueryRequest", 1) @@ -92,7 +92,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = false cs.ReturnQueryResponse = dataResponse - resp, err := cdt.Decorator.QueryData(req.Context(), qdr) + resp, err := cdt.MiddlewareHandler.QueryData(req.Context(), qdr) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleQueryRequest", 1) @@ -105,9 +105,9 @@ func TestCachingMiddleware(t *testing.T) { }) t.Run("with async queries", func(t *testing.T) { - asyncCdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares( + asyncCdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares( NewCachingMiddlewareWithFeatureManager(cs, featuremgmt.WithFeatures(featuremgmt.FlagAwsAsyncQueryCaching))), ) t.Run("If shoudCacheQuery returns true update cache function is called", func(t *testing.T) { @@ -128,7 +128,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = false cs.ReturnQueryResponse = dataResponse - resp, err := asyncCdt.Decorator.QueryData(req.Context(), qdr) + resp, err := asyncCdt.MiddlewareHandler.QueryData(req.Context(), qdr) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleQueryRequest", 1) @@ -158,7 +158,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = false cs.ReturnQueryResponse = dataResponse - resp, err := asyncCdt.Decorator.QueryData(req.Context(), qdr) + resp, err := asyncCdt.MiddlewareHandler.QueryData(req.Context(), qdr) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleQueryRequest", 1) @@ -196,10 +196,10 @@ func TestCachingMiddleware(t *testing.T) { } cs := caching.NewFakeOSSCachingService() - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCachingMiddleware(cs)), - clienttest.WithResourceResponses([]*backend.CallResourceResponse{simulatedPluginResponse}), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCachingMiddleware(cs)), + handlertest.WithResourceResponses([]*backend.CallResourceResponse{simulatedPluginResponse}), ) jsonDataMap := map[string]any{} @@ -235,7 +235,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = true cs.ReturnResourceResponse = dataResponse - err := cdt.Decorator.CallResource(req.Context(), crr, storeOneResponseCallResourceSender) + err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, storeOneResponseCallResourceSender) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleResourceRequest", 1) @@ -255,7 +255,7 @@ func TestCachingMiddleware(t *testing.T) { cs.ReturnHit = false cs.ReturnResourceResponse = dataResponse - err := cdt.Decorator.CallResource(req.Context(), crr, storeOneResponseCallResourceSender) + err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, storeOneResponseCallResourceSender) assert.NoError(t, err) // Cache service is called once cs.AssertCalls(t, "HandleResourceRequest", 1) @@ -272,9 +272,9 @@ func TestCachingMiddleware(t *testing.T) { require.NoError(t, err) cs := caching.NewFakeOSSCachingService() - cdt := clienttest.NewClientDecoratorTest(t, + cdt := handlertest.NewHandlerMiddlewareTest(t, // Skip the request context in this case - clienttest.WithMiddlewares(NewCachingMiddleware(cs)), + handlertest.WithMiddlewares(NewCachingMiddleware(cs)), ) reqCtx := contexthandler.FromContext(req.Context()) require.Nil(t, reqCtx) @@ -298,7 +298,7 @@ func TestCachingMiddleware(t *testing.T) { PluginContext: pluginCtx, } - resp, err := cdt.Decorator.QueryData(context.Background(), qdr) + resp, err := cdt.MiddlewareHandler.QueryData(context.Background(), qdr) assert.NoError(t, err) // Cache service is never called cs.AssertCalls(t, "HandleQueryRequest", 0) @@ -315,7 +315,7 @@ func TestCachingMiddleware(t *testing.T) { PluginContext: pluginCtx, } - err := cdt.Decorator.CallResource(req.Context(), crr, nopCallResourceSender) + err := cdt.MiddlewareHandler.CallResource(req.Context(), crr, nopCallResourceSender) assert.NoError(t, err) // Cache service is never called cs.AssertCalls(t, "HandleResourceRequest", 0) diff --git a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go index 1ec57e09f39..7a942eb4661 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware.go @@ -5,25 +5,22 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" ) -// NewClearAuthHeadersMiddleware creates a new plugins.ClientMiddleware +// NewClearAuthHeadersMiddleware creates a new backend.HandlerMiddleware // that will clear any outgoing HTTP headers that was part of the incoming // HTTP request and used when authenticating to Grafana. -func NewClearAuthHeadersMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewClearAuthHeadersMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &ClearAuthHeadersMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type ClearAuthHeadersMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *ClearAuthHeadersMiddleware) clearHeaders(ctx context.Context, h backend.ForwardHTTPHeaders) { @@ -43,30 +40,30 @@ func (m *ClearAuthHeadersMiddleware) clearHeaders(ctx context.Context, h backend func (m *ClearAuthHeadersMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } m.clearHeaders(ctx, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *ClearAuthHeadersMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } m.clearHeaders(ctx, req) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *ClearAuthHeadersMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } m.clearHeaders(ctx, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go index 2aa709ff987..a02e3d3500d 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/clear_auth_headers_middleware_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" @@ -23,9 +23,9 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { req.Header.Set(otherHeader, "test") t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewClearAuthHeadersMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -33,7 +33,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { } t.Run("Should not attach delete headers middleware when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -43,7 +43,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should not attach delete headers middleware when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -53,7 +53,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should not attach delete headers middleware when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -64,9 +64,9 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewClearAuthHeadersMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -74,7 +74,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { } t.Run("Should not attach delete headers middleware when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -84,7 +84,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should not attach delete headers middleware when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -94,7 +94,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should not attach delete headers middleware when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -110,9 +110,9 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { require.NoError(t, err) t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewClearAuthHeadersMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), ) req := req.WithContext(contexthandler.WithAuthHTTPHeaders(req.Context(), setting.NewCfg())) @@ -126,7 +126,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { } t.Run("Should attach delete headers middleware when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -137,7 +137,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should attach delete headers middleware when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -148,7 +148,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should attach delete headers middleware when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -160,9 +160,9 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewClearAuthHeadersMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewClearAuthHeadersMiddleware()), ) req := req.WithContext(contexthandler.WithAuthHTTPHeaders(req.Context(), setting.NewCfg())) @@ -176,7 +176,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { } t.Run("Should attach delete headers middleware when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -187,7 +187,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should attach delete headers middleware when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -198,7 +198,7 @@ func TestClearAuthHeadersMiddleware(t *testing.T) { }) t.Run("Should attach delete headers middleware when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go index 53325809c73..fbb6f7c23e7 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/contextual_logger_middleware.go @@ -6,21 +6,20 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" ) -// NewContextualLoggerMiddleware creates a new plugins.ClientMiddleware that adds +// NewContextualLoggerMiddleware creates a new backend.HandlerMiddleware that adds // a contextual logger to the request context. -func NewContextualLoggerMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewContextualLoggerMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &ContextualLoggerMiddleware{ - next: next, + BaseHandler: backend.NewBaseHandler(next), } }) } type ContextualLoggerMiddleware struct { - next plugins.Client + backend.BaseHandler } // instrumentContext adds a contextual logger with plugin and request details to the given context. @@ -45,53 +44,53 @@ func instrumentContext(ctx context.Context, pCtx backend.PluginContext) context. func (m *ContextualLoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *ContextualLoggerMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *ContextualLoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } func (m *ContextualLoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.CollectMetrics(ctx, req) + return m.BaseHandler.CollectMetrics(ctx, req) } func (m *ContextualLoggerMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.SubscribeStream(ctx, req) + return m.BaseHandler.SubscribeStream(ctx, req) } func (m *ContextualLoggerMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.PublishStream(ctx, req) + return m.BaseHandler.PublishStream(ctx, req) } func (m *ContextualLoggerMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.RunStream(ctx, req, sender) + return m.BaseHandler.RunStream(ctx, req, sender) } // ValidateAdmission implements backend.AdmissionHandler. func (m *ContextualLoggerMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.ValidateAdmission(ctx, req) + return m.BaseHandler.ValidateAdmission(ctx, req) } // MutateAdmission implements backend.AdmissionHandler. func (m *ContextualLoggerMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.MutateAdmission(ctx, req) + return m.BaseHandler.MutateAdmission(ctx, req) } // ConvertObject implements backend.AdmissionHandler. func (m *ContextualLoggerMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { ctx = instrumentContext(ctx, req.PluginContext) - return m.next.ConvertObjects(ctx, req) + return m.BaseHandler.ConvertObjects(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware.go index 73e1a1f3d78..fc1ba372ae5 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/util/proxyutil" @@ -14,22 +13,20 @@ import ( const cookieHeaderName = "Cookie" -// NewCookiesMiddleware creates a new plugins.ClientMiddleware that will -// forward incoming HTTP request Cookies to outgoing plugins.Client requests +// NewCookiesMiddleware creates a new backend.HandlerMiddleware that will +// forward incoming HTTP request Cookies to outgoing backend.Handler requests // if the datasource has enabled forwarding of cookies (keepCookies). -func NewCookiesMiddleware(skipCookiesNames []string) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewCookiesMiddleware(skipCookiesNames []string) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &CookiesMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), skipCookiesNames: skipCookiesNames, } }) } type CookiesMiddleware struct { - baseMiddleware + backend.BaseHandler skipCookiesNames []string } @@ -87,7 +84,7 @@ func (m *CookiesMiddleware) applyCookies(ctx context.Context, pCtx backend.Plugi func (m *CookiesMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } err := m.applyCookies(ctx, req.PluginContext, req) @@ -95,12 +92,12 @@ func (m *CookiesMiddleware) QueryData(ctx context.Context, req *backend.QueryDat return nil, err } - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *CookiesMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } err := m.applyCookies(ctx, req.PluginContext, req) @@ -108,12 +105,12 @@ func (m *CookiesMiddleware) CallResource(ctx context.Context, req *backend.CallR return err } - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *CookiesMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } err := m.applyCookies(ctx, req.PluginContext, req) @@ -121,5 +118,5 @@ func (m *CookiesMiddleware) CheckHealth(ctx context.Context, req *backend.CheckH return nil, err } - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware_test.go index 41822d6a4ba..778363b4936 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/cookies_middleware_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" ) @@ -28,9 +28,9 @@ func TestCookiesMiddleware(t *testing.T) { }) req.Header.Set(otherHeader, "test") - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), ) jsonDataMap := map[string]any{} @@ -44,7 +44,7 @@ func TestCookiesMiddleware(t *testing.T) { } t.Run("Should not forward cookies when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -60,7 +60,7 @@ func TestCookiesMiddleware(t *testing.T) { Headers: map[string][]string{otherHeader: {"test"}}, } pReq.Headers[backend.CookiesHeaderName] = []string{req.Header.Get(backend.CookiesHeaderName)} - err = cdt.Decorator.CallResource(req.Context(), pReq, nopCallResourceSender) + err = cdt.MiddlewareHandler.CallResource(req.Context(), pReq, nopCallResourceSender) require.NoError(t, err) require.NotNil(t, cdt.CallResourceReq) require.Len(t, cdt.CallResourceReq.Headers, 1) @@ -68,7 +68,7 @@ func TestCookiesMiddleware(t *testing.T) { }) t.Run("Should not forward cookies when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -97,9 +97,9 @@ func TestCookiesMiddleware(t *testing.T) { req.Header.Set(otherHeader, "test") - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), ) jsonDataMap := map[string]any{ @@ -115,7 +115,7 @@ func TestCookiesMiddleware(t *testing.T) { } t.Run("Should forward cookies when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -127,7 +127,7 @@ func TestCookiesMiddleware(t *testing.T) { }) t.Run("Should forward cookies when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -140,7 +140,7 @@ func TestCookiesMiddleware(t *testing.T) { }) t.Run("Should forward cookies when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -166,9 +166,9 @@ func TestCookiesMiddleware(t *testing.T) { }) req.Header.Set(otherHeader, "test") - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewCookiesMiddleware([]string{"grafana_session"})), ) pluginCtx := backend.PluginContext{ @@ -181,7 +181,7 @@ func TestCookiesMiddleware(t *testing.T) { Headers: map[string]string{otherHeader: "test"}, } pReq.Headers[backend.CookiesHeaderName] = req.Header.Get(backend.CookiesHeaderName) - _, err = cdt.Decorator.QueryData(req.Context(), pReq) + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), pReq) require.NoError(t, err) require.NotNil(t, cdt.QueryDataReq) require.Len(t, cdt.QueryDataReq.Headers, 1) @@ -194,7 +194,7 @@ func TestCookiesMiddleware(t *testing.T) { Headers: map[string][]string{otherHeader: {"test"}}, } pReq.Headers[backend.CookiesHeaderName] = []string{req.Header.Get(backend.CookiesHeaderName)} - err = cdt.Decorator.CallResource(req.Context(), pReq, nopCallResourceSender) + err = cdt.MiddlewareHandler.CallResource(req.Context(), pReq, nopCallResourceSender) require.NoError(t, err) require.NotNil(t, cdt.CallResourceReq) require.Len(t, cdt.CallResourceReq.Headers, 1) @@ -207,7 +207,7 @@ func TestCookiesMiddleware(t *testing.T) { Headers: map[string]string{otherHeader: "test"}, } pReq.Headers[backend.CookiesHeaderName] = req.Header.Get(backend.CookiesHeaderName) - _, err = cdt.Decorator.CheckHealth(req.Context(), pReq) + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), pReq) require.NoError(t, err) require.NotNil(t, cdt.CheckHealthReq) require.Len(t, cdt.CheckHealthReq.Headers, 1) diff --git a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go index 9e799759157..42764a7641f 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware.go @@ -5,26 +5,23 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" ) const forwardIDHeaderName = "X-Grafana-Id" -// NewForwardIDMiddleware creates a new plugins.ClientMiddleware that will -// set grafana id header on outgoing plugins.Client requests -func NewForwardIDMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +// NewForwardIDMiddleware creates a new backend.HandlerMiddleware that will +// set grafana id header on outgoing backend.Handler requests +func NewForwardIDMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &ForwardIDMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type ForwardIDMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *ForwardIDMiddleware) applyToken(ctx context.Context, pCtx backend.PluginContext, req backend.ForwardHTTPHeaders) error { @@ -43,7 +40,7 @@ func (m *ForwardIDMiddleware) applyToken(ctx context.Context, pCtx backend.Plugi func (m *ForwardIDMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } err := m.applyToken(ctx, req.PluginContext, req) @@ -51,12 +48,12 @@ func (m *ForwardIDMiddleware) QueryData(ctx context.Context, req *backend.QueryD return nil, err } - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *ForwardIDMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } err := m.applyToken(ctx, req.PluginContext, req) @@ -64,12 +61,12 @@ func (m *ForwardIDMiddleware) CallResource(ctx context.Context, req *backend.Cal return err } - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *ForwardIDMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } err := m.applyToken(ctx, req.PluginContext, req) @@ -77,5 +74,5 @@ func (m *ForwardIDMiddleware) CheckHealth(ctx context.Context, req *backend.Chec return nil, err } - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go index 2972c612a92..bc8532e7f75 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/forward_id_middleware_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" @@ -21,14 +21,14 @@ func TestForwardIDMiddleware(t *testing.T) { } t.Run("Should set forwarded id header if present", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewForwardIDMiddleware())) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{IDToken: "some-token"}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: pluginContext, }, nopCallResourceSender) require.NoError(t, err) @@ -37,14 +37,14 @@ func TestForwardIDMiddleware(t *testing.T) { }) t.Run("Should not set forwarded id header if not present", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewForwardIDMiddleware())) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: pluginContext, }, nopCallResourceSender) require.NoError(t, err) @@ -57,14 +57,14 @@ func TestForwardIDMiddleware(t *testing.T) { } t.Run("Should set forwarded id header to app plugin if present", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewForwardIDMiddleware())) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewForwardIDMiddleware())) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{IDToken: "some-token"}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: pluginContext, }, nopCallResourceSender) require.NoError(t, err) diff --git a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go index c6c4a5947a6..9a23168c6f5 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -24,24 +23,22 @@ const GrafanaSignedRequestID = "X-Grafana-Signed-Request-Id" const XRealIPHeader = "X-Real-Ip" const GrafanaInternalRequest = "X-Grafana-Internal-Request" -// NewHostedGrafanaACHeaderMiddleware creates a new plugins.ClientMiddleware that will +// NewHostedGrafanaACHeaderMiddleware creates a new backend.HandlerMiddleware that will // generate a random request ID, sign it using internal key and populate X-Grafana-Request-ID with the request ID // and X-Grafana-Signed-Request-ID with signed request ID. We can then use this to verify that the request // is coming from hosted Grafana and is not an external request. This is used for IP range access control. -func NewHostedGrafanaACHeaderMiddleware(cfg *setting.Cfg) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewHostedGrafanaACHeaderMiddleware(cfg *setting.Cfg) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &HostedGrafanaACHeaderMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, - log: log.New("ip_header_middleware"), - cfg: cfg, + BaseHandler: backend.NewBaseHandler(next), + log: log.New("ip_header_middleware"), + cfg: cfg, } }) } type HostedGrafanaACHeaderMiddleware struct { - baseMiddleware + backend.BaseHandler log log.Logger cfg *setting.Cfg } @@ -120,30 +117,30 @@ func GetGrafanaRequestIDHeaders(req *http.Request, cfg *setting.Cfg, logger log. func (m *HostedGrafanaACHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } m.applyGrafanaRequestIDHeader(ctx, req.PluginContext, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *HostedGrafanaACHeaderMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } m.applyGrafanaRequestIDHeader(ctx, req.PluginContext, req) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *HostedGrafanaACHeaderMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } m.applyGrafanaRequestIDHeader(ctx, req.PluginContext, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go index 3c389edea1d..01e398f4004 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/grafana_request_id_header_middleware_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/user" @@ -26,7 +26,7 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{ @@ -35,7 +35,7 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ URL: "https://logs.grafana.net", @@ -68,14 +68,14 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ URL: "https://logs.not-grafana.net", @@ -93,14 +93,14 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ URL: "https://logs.grafana.net/abc/../some/path", @@ -118,14 +118,14 @@ func Test_HostedGrafanaACHeaderMiddleware(t *testing.T) { allowedURL := &url.URL{Scheme: "https", Host: "logs.grafana.net"} cfg.IPRangeACAllowedURLs = []*url.URL{allowedURL} cfg.IPRangeACSecretKey = "secret" - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(NewHostedGrafanaACHeaderMiddleware(cfg))) ctx := context.WithValue(context.Background(), ctxkey.Key{}, &contextmodel.ReqContext{ Context: &web.Context{Req: &http.Request{}}, SignedInUser: &user.SignedInUser{}, }) - err := cdt.Decorator.CallResource(ctx, &backend.CallResourceRequest{ + err := cdt.MiddlewareHandler.CallResource(ctx, &backend.CallResourceRequest{ PluginContext: backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ URL: "https://logs.grafana.net", diff --git a/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware.go index 913ad4e0bf5..723c9788944 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware.go @@ -7,26 +7,23 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/plugins" ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ) const forwardPluginRequestHTTPHeaders = "forward-plugin-request-http-headers" -// NewHTTPClientMiddleware creates a new plugins.ClientMiddleware +// NewHTTPClientMiddleware creates a new backend.HandlerMiddleware // that will forward plugin request headers as outgoing HTTP headers. -func NewHTTPClientMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewHTTPClientMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &HTTPClientMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type HTTPClientMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *HTTPClientMiddleware) applyHeaders(ctx context.Context, pReq any) context.Context { @@ -69,30 +66,30 @@ func (m *HTTPClientMiddleware) applyHeaders(ctx context.Context, pReq any) conte func (m *HTTPClientMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } ctx = m.applyHeaders(ctx, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *HTTPClientMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } ctx = m.applyHeaders(ctx, req) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *HTTPClientMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } ctx = m.applyHeaders(ctx, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware_test.go index 58f3faf3c99..a61046b9150 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/httpclient_middleware_test.go @@ -7,8 +7,8 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/proxyutil" @@ -23,9 +23,9 @@ func TestHTTPClientMiddleware(t *testing.T) { require.NoError(t, err) t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewHTTPClientMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewHTTPClientMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -33,7 +33,7 @@ func TestHTTPClientMiddleware(t *testing.T) { } t.Run("Should not forward headers when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "val"}, }) @@ -53,7 +53,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not forward headers when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"val"}}, }, nopCallResourceSender) @@ -73,7 +73,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not forward headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "val"}, }) @@ -94,9 +94,9 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewHTTPClientMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewHTTPClientMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -104,7 +104,7 @@ func TestHTTPClientMiddleware(t *testing.T) { } t.Run("Should not forward headers when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -124,7 +124,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not forward headers when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -144,7 +144,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not forward headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -184,9 +184,9 @@ func TestHTTPClientMiddleware(t *testing.T) { } t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewHTTPClientMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewHTTPClientMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -194,7 +194,7 @@ func TestHTTPClientMiddleware(t *testing.T) { } t.Run("Should forward headers when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: headers, }) @@ -222,7 +222,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should forward headers when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: crHeaders, }, nopCallResourceSender) @@ -250,7 +250,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should forward headers when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: headers, }) @@ -278,7 +278,7 @@ func TestHTTPClientMiddleware(t *testing.T) { }) t.Run("Should not overwrite an existing header", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: headers, }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go index 526c13b95fa..ab1eb9f751a 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/logger_middleware.go @@ -7,21 +7,18 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/instrumentationutils" plog "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) -// NewLoggerMiddleware creates a new plugins.ClientMiddleware that will +// NewLoggerMiddleware creates a new backend.HandlerMiddleware that will // log requests. -func NewLoggerMiddleware(logger plog.Logger, pluginRegistry registry.Service) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewLoggerMiddleware(logger plog.Logger, pluginRegistry registry.Service) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &LoggerMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), logger: logger, pluginRegistry: pluginRegistry, } @@ -29,7 +26,7 @@ func NewLoggerMiddleware(logger plog.Logger, pluginRegistry registry.Service) pl } type LoggerMiddleware struct { - baseMiddleware + backend.BaseHandler logger plog.Logger pluginRegistry registry.Service } @@ -77,13 +74,13 @@ func (m *LoggerMiddleware) logRequest(ctx context.Context, pCtx backend.PluginCo func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } var resp *backend.QueryDataResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.QueryData(ctx, req) + resp, innerErr = m.BaseHandler.QueryData(ctx, req) if innerErr != nil { return instrumentationutils.RequestStatusFromError(innerErr), innerErr @@ -110,11 +107,11 @@ func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryData func (m *LoggerMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { - innerErr := m.next.CallResource(ctx, req, sender) + innerErr := m.BaseHandler.CallResource(ctx, req, sender) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -123,13 +120,13 @@ func (m *LoggerMiddleware) CallResource(ctx context.Context, req *backend.CallRe func (m *LoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } var resp *backend.CheckHealthResult err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.CheckHealth(ctx, req) + resp, innerErr = m.BaseHandler.CheckHealth(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -138,13 +135,13 @@ func (m *LoggerMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHe func (m *LoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { if req == nil { - return m.next.CollectMetrics(ctx, req) + return m.BaseHandler.CollectMetrics(ctx, req) } var resp *backend.CollectMetricsResult err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.CollectMetrics(ctx, req) + resp, innerErr = m.BaseHandler.CollectMetrics(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -153,13 +150,13 @@ func (m *LoggerMiddleware) CollectMetrics(ctx context.Context, req *backend.Coll func (m *LoggerMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { if req == nil { - return m.next.SubscribeStream(ctx, req) + return m.BaseHandler.SubscribeStream(ctx, req) } var resp *backend.SubscribeStreamResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.SubscribeStream(ctx, req) + resp, innerErr = m.BaseHandler.SubscribeStream(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -168,13 +165,13 @@ func (m *LoggerMiddleware) SubscribeStream(ctx context.Context, req *backend.Sub func (m *LoggerMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { if req == nil { - return m.next.PublishStream(ctx, req) + return m.BaseHandler.PublishStream(ctx, req) } var resp *backend.PublishStreamResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.PublishStream(ctx, req) + resp, innerErr = m.BaseHandler.PublishStream(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -183,11 +180,11 @@ func (m *LoggerMiddleware) PublishStream(ctx context.Context, req *backend.Publi func (m *LoggerMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { if req == nil { - return m.next.RunStream(ctx, req, sender) + return m.BaseHandler.RunStream(ctx, req, sender) } err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { - innerErr := m.next.RunStream(ctx, req, sender) + innerErr := m.BaseHandler.RunStream(ctx, req, sender) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -196,13 +193,13 @@ func (m *LoggerMiddleware) RunStream(ctx context.Context, req *backend.RunStream func (m *LoggerMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { if req == nil { - return m.next.ValidateAdmission(ctx, req) + return m.BaseHandler.ValidateAdmission(ctx, req) } var resp *backend.ValidationResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.ValidateAdmission(ctx, req) + resp, innerErr = m.BaseHandler.ValidateAdmission(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -211,13 +208,13 @@ func (m *LoggerMiddleware) ValidateAdmission(ctx context.Context, req *backend.A func (m *LoggerMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { if req == nil { - return m.next.MutateAdmission(ctx, req) + return m.BaseHandler.MutateAdmission(ctx, req) } var resp *backend.MutationResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.MutateAdmission(ctx, req) + resp, innerErr = m.BaseHandler.MutateAdmission(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -226,13 +223,13 @@ func (m *LoggerMiddleware) MutateAdmission(ctx context.Context, req *backend.Adm func (m *LoggerMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { if req == nil { - return m.next.ConvertObjects(ctx, req) + return m.BaseHandler.ConvertObjects(ctx, req) } var resp *backend.ConversionResponse err := m.logRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.ConvertObjects(ctx, req) + resp, innerErr = m.BaseHandler.ConvertObjects(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go index a93c0a13a2a..29e7b1ec998 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware.go @@ -25,7 +25,7 @@ type pluginMetrics struct { // MetricsMiddleware is a middleware that instruments plugin requests. // It tracks requests count, duration and size as prometheus metrics. type MetricsMiddleware struct { - baseMiddleware + backend.BaseHandler pluginMetrics pluginRegistry registry.Service } @@ -75,12 +75,10 @@ func newMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry r } // NewMetricsMiddleware returns a new MetricsMiddleware. -func NewMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service) plugins.ClientMiddleware { +func NewMetricsMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service) backend.HandlerMiddleware { imw := newMetricsMiddleware(promRegisterer, pluginRegistry) - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - imw.baseMiddleware = baseMiddleware{ - next: next, - } + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { + imw.BaseHandler = backend.NewBaseHandler(next) return imw }) } @@ -154,7 +152,7 @@ func (m *MetricsMiddleware) QueryData(ctx context.Context, req *backend.QueryDat var resp *backend.QueryDataResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.QueryData(ctx, req) + resp, innerErr = m.BaseHandler.QueryData(ctx, req) return instrumentationutils.RequestStatusFromQueryDataResponse(resp, innerErr), innerErr }) @@ -166,7 +164,7 @@ func (m *MetricsMiddleware) CallResource(ctx context.Context, req *backend.CallR return err } return m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { - innerErr := m.next.CallResource(ctx, req, sender) + innerErr := m.BaseHandler.CallResource(ctx, req, sender) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) } @@ -175,7 +173,7 @@ func (m *MetricsMiddleware) CheckHealth(ctx context.Context, req *backend.CheckH var resp *backend.CheckHealthResult err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.CheckHealth(ctx, req) + resp, innerErr = m.BaseHandler.CheckHealth(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -186,7 +184,7 @@ func (m *MetricsMiddleware) CollectMetrics(ctx context.Context, req *backend.Col var resp *backend.CollectMetricsResult err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.CollectMetrics(ctx, req) + resp, innerErr = m.BaseHandler.CollectMetrics(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) return resp, err @@ -196,7 +194,7 @@ func (m *MetricsMiddleware) SubscribeStream(ctx context.Context, req *backend.Su var resp *backend.SubscribeStreamResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.SubscribeStream(ctx, req) + resp, innerErr = m.BaseHandler.SubscribeStream(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) return resp, err @@ -206,7 +204,7 @@ func (m *MetricsMiddleware) PublishStream(ctx context.Context, req *backend.Publ var resp *backend.PublishStreamResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.PublishStream(ctx, req) + resp, innerErr = m.BaseHandler.PublishStream(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) return resp, err @@ -214,7 +212,7 @@ func (m *MetricsMiddleware) PublishStream(ctx context.Context, req *backend.Publ func (m *MetricsMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { - innerErr := m.next.RunStream(ctx, req, sender) + innerErr := m.BaseHandler.RunStream(ctx, req, sender) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) return err @@ -224,7 +222,7 @@ func (m *MetricsMiddleware) ValidateAdmission(ctx context.Context, req *backend. var resp *backend.ValidationResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.ValidateAdmission(ctx, req) + resp, innerErr = m.BaseHandler.ValidateAdmission(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -235,7 +233,7 @@ func (m *MetricsMiddleware) MutateAdmission(ctx context.Context, req *backend.Ad var resp *backend.MutationResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.MutateAdmission(ctx, req) + resp, innerErr = m.BaseHandler.MutateAdmission(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) @@ -246,7 +244,7 @@ func (m *MetricsMiddleware) ConvertObjects(ctx context.Context, req *backend.Con var resp *backend.ConversionResponse err := m.instrumentPluginRequest(ctx, req.PluginContext, func(ctx context.Context) (instrumentationutils.RequestStatus, error) { var innerErr error - resp, innerErr = m.next.ConvertObjects(ctx, req) + resp, innerErr = m.BaseHandler.ConvertObjects(ctx, req) return instrumentationutils.RequestStatusFromError(innerErr), innerErr }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go index 40c26cef09a..1af03e7af79 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/metrics_middleware_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" dto "github.com/prometheus/client_model/go" @@ -15,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/plugins/instrumentationutils" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) @@ -34,36 +34,36 @@ func TestInstrumentationMiddleware(t *testing.T) { t.Run("should instrument requests", func(t *testing.T) { for _, tc := range []struct { expEndpoint backend.Endpoint - fn func(cdt *clienttest.ClientDecoratorTest) error + fn func(cdt *handlertest.HandlerMiddlewareTest) error shouldInstrumentRequestSize bool }{ { expEndpoint: backend.EndpointCheckHealth, - fn: func(cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.CheckHealth(context.Background(), &backend.CheckHealthRequest{PluginContext: pCtx}) + fn: func(cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.CheckHealth(context.Background(), &backend.CheckHealthRequest{PluginContext: pCtx}) return err }, shouldInstrumentRequestSize: false, }, { expEndpoint: backend.EndpointCallResource, - fn: func(cdt *clienttest.ClientDecoratorTest) error { - return cdt.Decorator.CallResource(context.Background(), &backend.CallResourceRequest{PluginContext: pCtx}, nopCallResourceSender) + fn: func(cdt *handlertest.HandlerMiddlewareTest) error { + return cdt.MiddlewareHandler.CallResource(context.Background(), &backend.CallResourceRequest{PluginContext: pCtx}, nopCallResourceSender) }, shouldInstrumentRequestSize: true, }, { expEndpoint: backend.EndpointQueryData, - fn: func(cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) + fn: func(cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) return err }, shouldInstrumentRequestSize: true, }, { expEndpoint: backend.EndpointCollectMetrics, - fn: func(cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{PluginContext: pCtx}) + fn: func(cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{PluginContext: pCtx}) return err }, shouldInstrumentRequestSize: false, @@ -77,9 +77,9 @@ func TestInstrumentationMiddleware(t *testing.T) { })) mw := newMetricsMiddleware(promRegistry, pluginsRegistry) - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares( - plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - mw.next = next + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares( + backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { + mw.BaseHandler = backend.NewBaseHandler(next) return mw }), )) @@ -154,10 +154,10 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { JSONData: plugins.JSONData{ID: pluginID, Backend: true}, })) metricsMw := newMetricsMiddleware(promRegistry, pluginsRegistry) - cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares( + cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares( NewPluginRequestMetaMiddleware(), - plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - metricsMw.next = next + backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { + metricsMw.BaseHandler = backend.NewBaseHandler(next) return metricsMw }), NewStatusSourceMiddleware(), @@ -166,10 +166,10 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { t.Run("Metrics", func(t *testing.T) { metricsMw.pluginMetrics.pluginRequestCounter.Reset() - cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + cdt.TestHandler.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { return &backend.QueryDataResponse{Responses: map[string]backend.DataResponse{"A": downstreamErrorResponse}}, nil } - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) require.NoError(t, err) counter, err := metricsMw.pluginMetrics.pluginRequestCounter.GetMetricWith(newLabels( queryDataErrorCounterLabels, @@ -235,12 +235,12 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) { cdt.QueryDataCtx = nil cdt.QueryDataReq = nil }) - cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + cdt.TestHandler.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { cdt.QueryDataCtx = ctx cdt.QueryDataReq = req return &backend.QueryDataResponse{Responses: tc.responses}, nil } - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) require.NoError(t, err) ctxStatusSource := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) require.Equal(t, tc.expStatusSource, ctxStatusSource) diff --git a/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware.go index b37a6f88e95..dcf59a50714 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware.go @@ -7,28 +7,25 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/oauthtoken" ) -// NewOAuthTokenMiddleware creates a new plugins.ClientMiddleware that will -// set OAuth token headers on outgoing plugins.Client requests if the +// NewOAuthTokenMiddleware creates a new backend.HandlerMiddleware that will +// set OAuth token headers on outgoing backend.Handler requests if the // datasource has enabled Forward OAuth Identity (oauthPassThru). -func NewOAuthTokenMiddleware(oAuthTokenService oauthtoken.OAuthTokenService) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewOAuthTokenMiddleware(oAuthTokenService oauthtoken.OAuthTokenService) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &OAuthTokenMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), oAuthTokenService: oAuthTokenService, } }) } type OAuthTokenMiddleware struct { - baseMiddleware + backend.BaseHandler oAuthTokenService oauthtoken.OAuthTokenService } @@ -87,7 +84,7 @@ func (m *OAuthTokenMiddleware) applyToken(ctx context.Context, pCtx backend.Plug func (m *OAuthTokenMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } err := m.applyToken(ctx, req.PluginContext, req) @@ -95,12 +92,12 @@ func (m *OAuthTokenMiddleware) QueryData(ctx context.Context, req *backend.Query return nil, err } - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *OAuthTokenMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } err := m.applyToken(ctx, req.PluginContext, req) @@ -108,12 +105,12 @@ func (m *OAuthTokenMiddleware) CallResource(ctx context.Context, req *backend.Ca return err } - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *OAuthTokenMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } err := m.applyToken(ctx, req.PluginContext, req) @@ -121,5 +118,5 @@ func (m *OAuthTokenMiddleware) CheckHealth(ctx context.Context, req *backend.Che return nil, err } - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware_test.go index 95e0a4212e2..abbf7be4813 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/oauthtoken_middleware_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" @@ -23,9 +23,9 @@ func TestOAuthTokenMiddleware(t *testing.T) { req.Header.Set(otherHeader, "test") oAuthTokenService := &oauthtokentest.Service{} - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewOAuthTokenMiddleware(oAuthTokenService)), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewOAuthTokenMiddleware(oAuthTokenService)), ) jsonDataMap := map[string]any{} @@ -39,7 +39,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { } t.Run("Should not forward OAuth Identity when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -50,7 +50,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { }) t.Run("Should not forward OAuth Identity when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -61,7 +61,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { }) t.Run("Should not forward OAuth Identity when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -86,9 +86,9 @@ func TestOAuthTokenMiddleware(t *testing.T) { oAuthTokenService := &oauthtokentest.Service{ Token: token, } - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{}), - clienttest.WithMiddlewares(NewOAuthTokenMiddleware(oAuthTokenService)), + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{}), + handlertest.WithMiddlewares(NewOAuthTokenMiddleware(oAuthTokenService)), ) jsonDataMap := map[string]any{ @@ -104,7 +104,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { } t.Run("Should forward OAuth Identity when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) @@ -117,7 +117,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { }) t.Run("Should forward OAuth Identity when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{otherHeader: {"test"}}, }, nopCallResourceSender) @@ -132,7 +132,7 @@ func TestOAuthTokenMiddleware(t *testing.T) { }) t.Run("Should forward OAuth Identity when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{otherHeader: "test"}, }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go index 6536df9ee12..d24e5dc1472 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware.go @@ -5,24 +5,23 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) -// NewPluginRequestMetaMiddleware returns a new plugins.ClientMiddleware that sets up the default +// NewPluginRequestMetaMiddleware returns a new backend.HandlerMiddleware that sets up the default // values for the plugin request meta in the context.Context. All middlewares that are executed // after this one are be able to access plugin request meta via the pluginrequestmeta package. -func NewPluginRequestMetaMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewPluginRequestMetaMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &PluginRequestMetaMiddleware{ - next: next, + BaseHandler: backend.NewBaseHandler(next), defaultStatusSource: pluginrequestmeta.DefaultStatusSource, } }) } type PluginRequestMetaMiddleware struct { - next plugins.Client + backend.BaseHandler defaultStatusSource pluginrequestmeta.StatusSource } @@ -35,53 +34,53 @@ func (m *PluginRequestMetaMiddleware) withDefaultPluginRequestMeta(ctx context.C func (m *PluginRequestMetaMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *PluginRequestMetaMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *PluginRequestMetaMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } func (m *PluginRequestMetaMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.CollectMetrics(ctx, req) + return m.BaseHandler.CollectMetrics(ctx, req) } func (m *PluginRequestMetaMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.SubscribeStream(ctx, req) + return m.BaseHandler.SubscribeStream(ctx, req) } func (m *PluginRequestMetaMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.PublishStream(ctx, req) + return m.BaseHandler.PublishStream(ctx, req) } func (m *PluginRequestMetaMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.RunStream(ctx, req, sender) + return m.BaseHandler.RunStream(ctx, req, sender) } // ValidateAdmission implements backend.AdmissionHandler. func (m *PluginRequestMetaMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.ValidateAdmission(ctx, req) + return m.BaseHandler.ValidateAdmission(ctx, req) } // MutateAdmission implements backend.AdmissionHandler. func (m *PluginRequestMetaMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.MutateAdmission(ctx, req) + return m.BaseHandler.MutateAdmission(ctx, req) } // ConvertObject implements backend.AdmissionHandler. func (m *PluginRequestMetaMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { ctx = m.withDefaultPluginRequestMeta(ctx) - return m.next.ConvertObjects(ctx, req) + return m.BaseHandler.ConvertObjects(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go index 79adb858313..b87fcfb84d5 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/plugin_request_meta_middleware_test.go @@ -5,34 +5,33 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) func TestPluginRequestMetaMiddleware(t *testing.T) { t.Run("default", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithMiddlewares(NewPluginRequestMetaMiddleware()), + cdt := handlertest.NewHandlerMiddlewareTest(t, + handlertest.WithMiddlewares(NewPluginRequestMetaMiddleware()), ) - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{}) + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) require.NoError(t, err) ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) require.Equal(t, pluginrequestmeta.StatusSourcePlugin, ss) }) t.Run("other value", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithMiddlewares(plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { + cdt := handlertest.NewHandlerMiddlewareTest(t, + handlertest.WithMiddlewares(backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &PluginRequestMetaMiddleware{ - next: next, + BaseHandler: backend.NewBaseHandler(next), defaultStatusSource: "test", } })), ) - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{}) + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) require.NoError(t, err) ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) require.Equal(t, pluginrequestmeta.StatusSource("test"), ss) diff --git a/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware.go deleted file mode 100644 index b000a549656..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware.go +++ /dev/null @@ -1,52 +0,0 @@ -package clientmiddleware - -import ( - "context" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/util/proxyutil" -) - -// NewResourceResponseMiddleware creates a new plugins.ClientMiddleware -// that will enforce HTTP header rules for backend.CallResourceResponse's. -func NewResourceResponseMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { - return &ResourceResponseMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, - } - }) -} - -type ResourceResponseMiddleware struct { - baseMiddleware -} - -func (m *ResourceResponseMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - return m.next.QueryData(ctx, req) -} - -func (m *ResourceResponseMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - if req == nil || sender == nil { - return m.next.CallResource(ctx, req, sender) - } - - processedStreams := 0 - wrappedSender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { - if processedStreams == 0 { - if res.Headers == nil { - res.Headers = map[string][]string{} - } - - proxyutil.SetProxyResponseHeaders(res.Headers) - } - - processedStreams++ - return sender.Send(res) - }) - - return m.next.CallResource(ctx, req, wrappedSender) -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware_test.go deleted file mode 100644 index 905977b32a4..00000000000 --- a/pkg/services/pluginsintegration/clientmiddleware/resource_response_middleware_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package clientmiddleware - -import ( - "context" - "net/http" - "testing" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" - "github.com/stretchr/testify/require" -) - -func TestResourceResponseMiddleware(t *testing.T) { - t.Run("Should set proxy response headers when calling CallResource", func(t *testing.T) { - crResp := &backend.CallResourceResponse{ - Status: http.StatusOK, - Headers: map[string][]string{ - "X-Custom": {"Should not be deleted"}, - }, - } - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithMiddlewares(NewResourceResponseMiddleware()), - clienttest.WithResourceResponses([]*backend.CallResourceResponse{crResp}), - ) - - var sentResponse *backend.CallResourceResponse - sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { - sentResponse = res - return nil - }) - - err := cdt.Decorator.CallResource(context.Background(), &backend.CallResourceRequest{ - PluginContext: backend.PluginContext{}, - }, sender) - require.NoError(t, err) - - require.NotNil(t, sentResponse) - require.Equal(t, "sandbox", sentResponse.Headers["Content-Security-Policy"][0]) - require.Equal(t, "Should not be deleted", sentResponse.Headers["X-Custom"][0]) - }) -} diff --git a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go index 5cd88c03aa6..d9af8df3887 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware.go @@ -6,30 +6,27 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) -// NewStatusSourceMiddleware returns a new plugins.ClientMiddleware that sets the status source in the +// NewStatusSourceMiddleware returns a new backend.HandlerMiddleware that sets the status source in the // plugin request meta stored in the context.Context, according to the query data responses returned by QueryError. // If at least one query data response has a "downstream" status source and there isn't one with a "plugin" status source, // the plugin request meta in the context is set to "downstream". -func NewStatusSourceMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewStatusSourceMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &StatusSourceMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type StatusSourceMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *StatusSourceMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - resp, err := m.next.QueryData(ctx, req) + resp, err := m.BaseHandler.QueryData(ctx, req) if resp == nil || len(resp.Responses) == 0 { return resp, err } diff --git a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go index 88d5e7f0534..c3544d4e9df 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/status_source_middleware_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/plugins/pluginrequestmeta" ) @@ -68,18 +68,18 @@ func TestStatusSourceMiddleware(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithMiddlewares( + cdt := handlertest.NewHandlerMiddlewareTest(t, + handlertest.WithMiddlewares( NewPluginRequestMetaMiddleware(), NewStatusSourceMiddleware(), ), ) - cdt.TestClient.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + cdt.TestHandler.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { cdt.QueryDataCtx = ctx return tc.queryDataResponse, nil } - _, _ = cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{}) + _, _ = cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{}) ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx) require.Equal(t, tc.expStatusSource, ss) diff --git a/pkg/services/pluginsintegration/clientmiddleware/testing.go b/pkg/services/pluginsintegration/clientmiddleware/testing.go index 391783cc0ef..0409fe9333b 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/testing.go +++ b/pkg/services/pluginsintegration/clientmiddleware/testing.go @@ -1,6 +1,31 @@ package clientmiddleware -import "github.com/grafana/grafana-plugin-sdk-go/backend" +import ( + "net/http" + "net/http/httptest" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" + "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/web" +) + +func WithReqContext(req *http.Request, user *user.SignedInUser) handlertest.HandlerMiddlewareTestOption { + return handlertest.HandlerMiddlewareTestOption(func(cdt *handlertest.HandlerMiddlewareTest) { + reqContext := &contextmodel.ReqContext{ + Context: &web.Context{ + Req: req, + Resp: web.NewResponseWriter(req.Method, httptest.NewRecorder()), + }, + SignedInUser: user, + } + + ctx := ctxkey.Set(req.Context(), reqContext) + *req = *req.WithContext(ctx) + }) +} var nopCallResourceSender = backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error { return nil diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go index 30f9b47033a..035ae7f48cb 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go @@ -5,28 +5,25 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/query" ) -// NewTracingHeaderMiddleware creates a new plugins.ClientMiddleware that will -// populate useful tracing headers on outgoing plugins.Client and HTTP +// NewTracingHeaderMiddleware creates a new backend.HandlerMiddleware that will +// populate useful tracing headers on outgoing backend.Handler and HTTP // requests. // Tracing headers are X-Datasource-Uid, X-Dashboard-Uid, // X-Panel-Id, X-Grafana-Org-Id. -func NewTracingHeaderMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewTracingHeaderMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &TracingHeaderMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type TracingHeaderMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *TracingHeaderMiddleware) applyHeaders(ctx context.Context, req backend.ForwardHTTPHeaders) { @@ -49,22 +46,22 @@ func (m *TracingHeaderMiddleware) applyHeaders(ctx context.Context, req backend. func (m *TracingHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } m.applyHeaders(ctx, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *TracingHeaderMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *TracingHeaderMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } m.applyHeaders(ctx, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go index b190dcc86fa..7b1df5a8065 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" ) @@ -25,15 +25,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { } t.Run("tracing headers are not set for query data", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -43,15 +43,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { }) t.Run("tracing headers are not set for health check", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -69,15 +69,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { } t.Run("tracing headers are not set for query data", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -87,15 +87,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { }) t.Run("tracing headers are not set for health check", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -120,15 +120,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { } t.Run("tracing headers are set for query data", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -144,15 +144,15 @@ func TestTracingHeaderMiddleware(t *testing.T) { }) t.Run("tracing headers are set for health check", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + handlertest.WithMiddlewares(NewTracingHeaderMiddleware()), ) - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware.go index d34d1635053..a4250de0047 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware.go @@ -11,24 +11,23 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/query" ) // NewTracingMiddleware returns a new middleware that creates a new span on every method call. -func NewTracingMiddleware(tracer tracing.Tracer) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func NewTracingMiddleware(tracer tracing.Tracer) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &TracingMiddleware{ - tracer: tracer, - next: next, + tracer: tracer, + BaseHandler: backend.NewBaseHandler(next), } }) } type TracingMiddleware struct { + backend.BaseHandler tracer tracing.Tracer - next plugins.Client } // setSpanAttributeFromHTTPHeader takes a ReqContext and a span, and adds the specified HTTP header as a span attribute @@ -84,7 +83,7 @@ func (m *TracingMiddleware) QueryData(ctx context.Context, req *backend.QueryDat var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.QueryData(ctx, req) + resp, err := m.BaseHandler.QueryData(ctx, req) return resp, err } @@ -92,7 +91,7 @@ func (m *TracingMiddleware) CallResource(ctx context.Context, req *backend.CallR var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - err = m.next.CallResource(ctx, req, sender) + err = m.BaseHandler.CallResource(ctx, req, sender) return err } @@ -100,7 +99,7 @@ func (m *TracingMiddleware) CheckHealth(ctx context.Context, req *backend.CheckH var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.CheckHealth(ctx, req) + resp, err := m.BaseHandler.CheckHealth(ctx, req) return resp, err } @@ -108,7 +107,7 @@ func (m *TracingMiddleware) CollectMetrics(ctx context.Context, req *backend.Col var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.CollectMetrics(ctx, req) + resp, err := m.BaseHandler.CollectMetrics(ctx, req) return resp, err } @@ -116,7 +115,7 @@ func (m *TracingMiddleware) SubscribeStream(ctx context.Context, req *backend.Su var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.SubscribeStream(ctx, req) + resp, err := m.BaseHandler.SubscribeStream(ctx, req) return resp, err } @@ -124,7 +123,7 @@ func (m *TracingMiddleware) PublishStream(ctx context.Context, req *backend.Publ var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.PublishStream(ctx, req) + resp, err := m.BaseHandler.PublishStream(ctx, req) return resp, err } @@ -132,7 +131,7 @@ func (m *TracingMiddleware) RunStream(ctx context.Context, req *backend.RunStrea var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - err = m.next.RunStream(ctx, req, sender) + err = m.BaseHandler.RunStream(ctx, req, sender) return err } @@ -141,7 +140,7 @@ func (m *TracingMiddleware) ValidateAdmission(ctx context.Context, req *backend. var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.ValidateAdmission(ctx, req) + resp, err := m.BaseHandler.ValidateAdmission(ctx, req) return resp, err } @@ -150,7 +149,7 @@ func (m *TracingMiddleware) MutateAdmission(ctx context.Context, req *backend.Ad var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.MutateAdmission(ctx, req) + resp, err := m.BaseHandler.MutateAdmission(ctx, req) return resp, err } @@ -159,6 +158,6 @@ func (m *TracingMiddleware) ConvertObjects(ctx context.Context, req *backend.Con var err error ctx, end := m.traceWrap(ctx, req.PluginContext) defer func() { end(err) }() - resp, err := m.next.ConvertObjects(ctx, req) + resp, err := m.BaseHandler.ConvertObjects(ctx, req) return resp, err } diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware_test.go index 249a66bd94f..78441fa5b8d 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_middleware_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/attribute" @@ -16,8 +17,6 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.17.0" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" "github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/web" @@ -30,13 +29,13 @@ func TestTracingMiddleware(t *testing.T) { for _, tc := range []struct { name string - run func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error + run func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error expSpanName string }{ { name: "QueryData", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{ PluginContext: pluginCtx, }) return err @@ -45,8 +44,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "CallResource", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - return cdt.Decorator.CallResource(context.Background(), &backend.CallResourceRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + return cdt.MiddlewareHandler.CallResource(context.Background(), &backend.CallResourceRequest{ PluginContext: pluginCtx, }, nopCallResourceSender) }, @@ -54,8 +53,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "CheckHealth", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.CheckHealth(context.Background(), &backend.CheckHealthRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.CheckHealth(context.Background(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, }) return err @@ -64,8 +63,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "CollectMetrics", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{ PluginContext: pluginCtx, }) return err @@ -74,8 +73,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "SubscribeStream", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.SubscribeStream(context.Background(), &backend.SubscribeStreamRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.SubscribeStream(context.Background(), &backend.SubscribeStreamRequest{ PluginContext: pluginCtx, }) return err @@ -84,8 +83,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "PublishStream", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - _, err := cdt.Decorator.PublishStream(context.Background(), &backend.PublishStreamRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + _, err := cdt.MiddlewareHandler.PublishStream(context.Background(), &backend.PublishStreamRequest{ PluginContext: pluginCtx, }) return err @@ -94,8 +93,8 @@ func TestTracingMiddleware(t *testing.T) { }, { name: "RunStream", - run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { - return cdt.Decorator.RunStream(context.Background(), &backend.RunStreamRequest{ + run: func(pluginCtx backend.PluginContext, cdt *handlertest.HandlerMiddlewareTest) error { + return cdt.MiddlewareHandler.RunStream(context.Background(), &backend.RunStreamRequest{ PluginContext: pluginCtx, }, &backend.StreamSender{}) }, @@ -107,9 +106,9 @@ func TestTracingMiddleware(t *testing.T) { spanRecorder := tracetest.NewSpanRecorder() tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) - cdt := clienttest.NewClientDecoratorTest( + cdt := handlertest.NewHandlerMiddlewareTest( t, - clienttest.WithMiddlewares(NewTracingMiddleware(tracer)), + handlertest.WithMiddlewares(NewTracingMiddleware(tracer)), ) err := tc.run(pluginCtx, cdt) @@ -126,9 +125,9 @@ func TestTracingMiddleware(t *testing.T) { spanRecorder := tracetest.NewSpanRecorder() tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) - cdt := clienttest.NewClientDecoratorTest( + cdt := handlertest.NewHandlerMiddlewareTest( t, - clienttest.WithMiddlewares( + handlertest.WithMiddlewares( NewTracingMiddleware(tracer), newAlwaysErrorMiddleware(errors.New("ops")), ), @@ -150,9 +149,9 @@ func TestTracingMiddleware(t *testing.T) { spanRecorder := tracetest.NewSpanRecorder() tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) - cdt := clienttest.NewClientDecoratorTest( + cdt := handlertest.NewHandlerMiddlewareTest( t, - clienttest.WithMiddlewares( + handlertest.WithMiddlewares( NewTracingMiddleware(tracer), newAlwaysPanicMiddleware("panic!"), ), @@ -314,12 +313,12 @@ func TestTracingMiddlewareAttributes(t *testing.T) { spanRecorder := tracetest.NewSpanRecorder() tracer := tracing.InitializeTracerForTest(tracing.WithSpanProcessor(spanRecorder)) - cdt := clienttest.NewClientDecoratorTest( + cdt := handlertest.NewHandlerMiddlewareTest( t, - clienttest.WithMiddlewares(NewTracingMiddleware(tracer)), + handlertest.WithMiddlewares(NewTracingMiddleware(tracer)), ) - _, err := cdt.Decorator.QueryData(ctx, req) + _, err := cdt.MiddlewareHandler.QueryData(ctx, req) require.NoError(t, err) spans := spanRecorder.Ended() require.Len(t, spans, 1, "must have 1 span") @@ -403,8 +402,8 @@ func (m *alwaysErrorFuncMiddleware) ConvertObjects(ctx context.Context, req *bac } // newAlwaysErrorMiddleware returns a new middleware that always returns the specified error. -func newAlwaysErrorMiddleware(err error) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func newAlwaysErrorMiddleware(err error) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &alwaysErrorFuncMiddleware{func() error { return err }} @@ -412,8 +411,8 @@ func newAlwaysErrorMiddleware(err error) plugins.ClientMiddleware { } // newAlwaysPanicMiddleware returns a new middleware that always panics with the specified message, -func newAlwaysPanicMiddleware(message string) plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +func newAlwaysPanicMiddleware(message string) backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &alwaysErrorFuncMiddleware{func() error { panic(message) }} diff --git a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go index 214b1305979..c3dbde59567 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go @@ -6,25 +6,22 @@ import ( "github.com/grafana/authlib/claims" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/util/proxyutil" ) -// NewUserHeaderMiddleware creates a new plugins.ClientMiddleware that will -// populate the X-Grafana-User header on outgoing plugins.Client requests. -func NewUserHeaderMiddleware() plugins.ClientMiddleware { - return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { +// NewUserHeaderMiddleware creates a new backend.HandlerMiddleware that will +// populate the X-Grafana-User header on outgoing backend.Handler requests. +func NewUserHeaderMiddleware() backend.HandlerMiddleware { + return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler { return &UserHeaderMiddleware{ - baseMiddleware: baseMiddleware{ - next: next, - }, + BaseHandler: backend.NewBaseHandler(next), } }) } type UserHeaderMiddleware struct { - baseMiddleware + backend.BaseHandler } func (m *UserHeaderMiddleware) applyUserHeader(ctx context.Context, h backend.ForwardHTTPHeaders) { @@ -42,30 +39,30 @@ func (m *UserHeaderMiddleware) applyUserHeader(ctx context.Context, h backend.Fo func (m *UserHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { if req == nil { - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } m.applyUserHeader(ctx, req) - return m.next.QueryData(ctx, req) + return m.BaseHandler.QueryData(ctx, req) } func (m *UserHeaderMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { if req == nil { - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } m.applyUserHeader(ctx, req) - return m.next.CallResource(ctx, req, sender) + return m.BaseHandler.CallResource(ctx, req, sender) } func (m *UserHeaderMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { if req == nil { - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } m.applyUserHeader(ctx, req) - return m.next.CheckHealth(ctx, req) + return m.BaseHandler.CheckHealth(ctx, req) } diff --git a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go index 9fedd33a0e2..327705bd1f6 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go +++ b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana-plugin-sdk-go/backend/handlertest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/stretchr/testify/require" @@ -17,12 +17,12 @@ func TestUserHeaderMiddleware(t *testing.T) { require.NoError(t, err) t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + handlertest.WithMiddlewares(NewUserHeaderMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -30,7 +30,7 @@ func TestUserHeaderMiddleware(t *testing.T) { } t.Run("Should not forward user header when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -40,7 +40,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should not forward user header when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -50,7 +50,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should not forward user header when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -61,12 +61,12 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ IsAnonymous: true, Login: "anonymous"}, ), - clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + handlertest.WithMiddlewares(NewUserHeaderMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -74,7 +74,7 @@ func TestUserHeaderMiddleware(t *testing.T) { } t.Run("Should not forward user header when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -84,7 +84,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should not forward user header when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -94,7 +94,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should not forward user header when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -110,11 +110,11 @@ func TestUserHeaderMiddleware(t *testing.T) { require.NoError(t, err) t.Run("And requests are for a datasource", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ Login: "admin", }), - clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + handlertest.WithMiddlewares(NewUserHeaderMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -122,7 +122,7 @@ func TestUserHeaderMiddleware(t *testing.T) { } t.Run("Should forward user header when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -133,7 +133,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should forward user header when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -144,7 +144,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should forward user header when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -156,11 +156,11 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("And requests are for an app", func(t *testing.T) { - cdt := clienttest.NewClientDecoratorTest(t, - clienttest.WithReqContext(req, &user.SignedInUser{ + cdt := handlertest.NewHandlerMiddlewareTest(t, + WithReqContext(req, &user.SignedInUser{ Login: "admin", }), - clienttest.WithMiddlewares(NewUserHeaderMiddleware()), + handlertest.WithMiddlewares(NewUserHeaderMiddleware()), ) pluginCtx := backend.PluginContext{ @@ -168,7 +168,7 @@ func TestUserHeaderMiddleware(t *testing.T) { } t.Run("Should forward user header when calling QueryData", func(t *testing.T) { - _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + _, err = cdt.MiddlewareHandler.QueryData(req.Context(), &backend.QueryDataRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) @@ -179,7 +179,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should forward user header when calling CallResource", func(t *testing.T) { - err = cdt.Decorator.CallResource(req.Context(), &backend.CallResourceRequest{ + err = cdt.MiddlewareHandler.CallResource(req.Context(), &backend.CallResourceRequest{ PluginContext: pluginCtx, Headers: map[string][]string{}, }, nopCallResourceSender) @@ -190,7 +190,7 @@ func TestUserHeaderMiddleware(t *testing.T) { }) t.Run("Should forward user header when calling CheckHealth", func(t *testing.T) { - _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + _, err = cdt.MiddlewareHandler.CheckHealth(req.Context(), &backend.CheckHealthRequest{ PluginContext: pluginCtx, Headers: map[string]string{}, }) diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index a699982efd8..36c20fa8cbc 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -3,6 +3,7 @@ package pluginsintegration import ( "github.com/google/wire" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/prometheus/client_golang/prometheus" "github.com/grafana/grafana/pkg/infra/tracing" @@ -139,13 +140,13 @@ var WireExtensionSet = wire.NewSet( wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), finder.ProvideLocalFinder, wire.Bind(new(finder.Finder), new(*finder.Local)), - ProvideClientDecorator, - wire.Bind(new(plugins.Client), new(*client.Decorator)), + ProvideClientWithMiddlewares, + wire.Bind(new(plugins.Client), new(*backend.MiddlewareHandler)), managedplugins.NewNoop, wire.Bind(new(managedplugins.Manager), new(*managedplugins.Noop)), ) -func ProvideClientDecorator( +func ProvideClientWithMiddlewares( cfg *setting.Cfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, @@ -153,23 +154,23 @@ func ProvideClientDecorator( cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, -) (*client.Decorator, error) { - return NewClientDecorator(cfg, pluginRegistry, oAuthTokenService, tracer, cachingService, features, promRegisterer, pluginRegistry) +) (*backend.MiddlewareHandler, error) { + return NewMiddlewareHandler(cfg, pluginRegistry, oAuthTokenService, tracer, cachingService, features, promRegisterer, pluginRegistry) } -func NewClientDecorator( +func NewMiddlewareHandler( cfg *setting.Cfg, pluginRegistry registry.Service, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service, -) (*client.Decorator, error) { +) (*backend.MiddlewareHandler, error) { c := client.ProvideService(pluginRegistry) middlewares := CreateMiddlewares(cfg, oAuthTokenService, tracer, cachingService, features, promRegisterer, registry) - return client.NewDecorator(c, middlewares...) + return backend.HandlerFromMiddlewares(c, middlewares...) } -func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service) []plugins.ClientMiddleware { - middlewares := []plugins.ClientMiddleware{ +func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service) []backend.HandlerMiddleware { + middlewares := []backend.HandlerMiddleware{ clientmiddleware.NewPluginRequestMetaMiddleware(), clientmiddleware.NewTracingMiddleware(tracer), clientmiddleware.NewMetricsMiddleware(promRegisterer, registry), @@ -187,7 +188,6 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken clientmiddleware.NewClearAuthHeadersMiddleware(), clientmiddleware.NewOAuthTokenMiddleware(oAuthTokenService), clientmiddleware.NewCookiesMiddleware(skipCookiesNames), - clientmiddleware.NewResourceResponseMiddleware(), clientmiddleware.NewCachingMiddlewareWithFeatureManager(cachingService, features), clientmiddleware.NewForwardIDMiddleware(), ) diff --git a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go index f80ca8213bc..90eb7e67a61 100644 --- a/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go +++ b/pkg/services/pluginsintegration/serviceregistration/serviceregistration.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/services/extsvcauth" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" + "github.com/grafana/grafana/pkg/setting" ) type Service struct { @@ -20,9 +21,10 @@ type Service struct { settingsSvc pluginsettings.Service } -func ProvideService(features featuremgmt.FeatureToggles, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { +func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, reg extsvcauth.ExternalServiceRegistry, settingsSvc pluginsettings.Service) *Service { + enabled := features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) && cfg.ManagedServiceAccountsEnabled s := &Service{ - featureEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), + featureEnabled: enabled, log: log.New("plugins.external.registration"), reg: reg, settingsSvc: settingsSvc, diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 6146c36de2f..e12350ddba4 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -31,6 +31,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources/guardian" datasourcesService "github.com/grafana/grafana/pkg/services/datasources/service" "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/folder/foldertest" "github.com/grafana/grafana/pkg/services/licensing/licensingtest" @@ -323,7 +324,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) dashService, err := service.ProvideDashboardServiceImpl( cfg, dashboardStoreService, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, - foldertest.NewFakeService(), nil, + foldertest.NewFakeService(), folder.NewFakeStore(), nil, ) require.NoError(t, err) diff --git a/pkg/services/searchV2/auth.go b/pkg/services/searchV2/auth.go index 5c23e6598ab..5d75fbfd24c 100644 --- a/pkg/services/searchV2/auth.go +++ b/pkg/services/searchV2/auth.go @@ -22,24 +22,24 @@ type FutureAuthService interface { var _ FutureAuthService = (*simpleAuthService)(nil) type simpleAuthService struct { - sql db.DB - ac accesscontrol.Service - folderService folder.Service - logger log.Logger + sql db.DB + ac accesscontrol.Service + folderStore folder.Store + logger log.Logger } func (a *simpleAuthService) GetDashboardReadFilter(ctx context.Context, orgID int64, user *user.SignedInUser) (ResourceFilter, error) { canReadDashboard, canReadFolder := accesscontrol.Checker(user, dashboards.ActionDashboardsRead), accesscontrol.Checker(user, dashboards.ActionFoldersRead) return func(kind entityKind, uid, parent string) bool { if kind == entityKindFolder { - scopes, err := dashboards.GetInheritedScopes(ctx, orgID, uid, a.folderService) + scopes, err := dashboards.GetInheritedScopes(ctx, orgID, uid, a.folderStore) if err != nil { a.logger.Debug("Could not retrieve inherited folder scopes:", "err", err) } scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(uid)) return canReadFolder(scopes...) } else if kind == entityKindDashboard { - scopes, err := dashboards.GetInheritedScopes(ctx, orgID, parent, a.folderService) + scopes, err := dashboards.GetInheritedScopes(ctx, orgID, parent, a.folderStore) if err != nil { a.logger.Debug("Could not retrieve inherited folder scopes:", "err", err) } diff --git a/pkg/services/searchV2/index_test.go b/pkg/services/searchV2/index_test.go index e6a569fb57c..976233f7a07 100644 --- a/pkg/services/searchV2/index_test.go +++ b/pkg/services/searchV2/index_test.go @@ -18,7 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -754,7 +754,7 @@ func setupIntegrationEnv(t *testing.T, folderCount, dashboardsPerFolder int, sql ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, } searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{}, - tracing.InitializeTracerForTest(), features, orgSvc, nil, foldertest.NewFakeService()).(*StandardSearchService) + tracing.InitializeTracerForTest(), features, orgSvc, nil, folder.NewFakeStore()).(*StandardSearchService) require.True(t, ok) err = runSearchService(searchService) diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index d886ec649a0..b0fbcfe72ca 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -85,7 +85,7 @@ func (s *StandardSearchService) IsReady(ctx context.Context, orgId int64) IsSear func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEventsService, ac accesscontrol.Service, tracer tracing.Tracer, features featuremgmt.FeatureToggles, orgService org.Service, - userService user.Service, folderService folder.Service) SearchService { + userService user.Service, folderStore folder.Store) SearchService { extender := &NoopExtender{} logger := log.New("searchV2") s := &StandardSearchService{ @@ -93,10 +93,10 @@ func ProvideService(cfg *setting.Cfg, sql db.DB, entityEventStore store.EntityEv sql: sql, ac: ac, auth: &simpleAuthService{ - sql: sql, - ac: ac, - folderService: folderService, - logger: logger, + sql: sql, + ac: ac, + folderStore: folderStore, + logger: logger, }, dashboardIndex: newSearchIndex( newSQLDashboardLoader(sql, tracer, cfg.Search), diff --git a/pkg/services/searchV2/service_bench_test.go b/pkg/services/searchV2/service_bench_test.go index 778e7bd2bf8..42a9849fd31 100644 --- a/pkg/services/searchV2/service_bench_test.go +++ b/pkg/services/searchV2/service_bench_test.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/store" @@ -42,7 +42,7 @@ func setupBenchEnv(b *testing.B, folderCount, dashboardsPerFolder int) (*Standar ExpectedOrgs: []*org.OrgDTO{{ID: 1}}, } searchService, ok := ProvideService(cfg, sqlStore, store.NewDummyEntityEventsService(), actest.FakeService{}, - tracing.InitializeTracerForTest(), features, orgSvc, nil, foldertest.NewFakeService()).(*StandardSearchService) + tracing.InitializeTracerForTest(), features, orgSvc, nil, folder.NewFakeStore()).(*StandardSearchService) require.True(b, ok) err = runSearchService(searchService) diff --git a/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go b/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go index 1293ff6313e..b2ad6dbf956 100644 --- a/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go +++ b/pkg/services/secrets/kvstore/migrations/to_plugin_mig_test.go @@ -100,12 +100,13 @@ func setupTestMigrateToPluginService(t *testing.T) (*MigrateToPluginService, sec raw, err := ini.Load([]byte(rawCfg)) require.NoError(t, err) cfg := &setting.Cfg{Raw: raw} + sqlStore := db.InitTestDB(t) + // this would be the plugin - mocked at the moment - fallbackStore := secretskvs.WithCache(secretskvs.NewFakeSQLSecretsKVStore(t), time.Minute*5, time.Minute*5) + fallbackStore := secretskvs.WithCache(secretskvs.NewFakeSQLSecretsKVStore(t, sqlStore), time.Minute*5, time.Minute*5) secretsStoreForPlugin := secretskvs.WithCache(secretskvs.NewFakePluginSecretsKVStore(t, featuremgmt.WithFeatures(), fallbackStore), time.Minute*5, time.Minute*5) // this is to init the sql secret store inside the migration - sqlStore := db.InitTestDB(t) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) manager := secretskvs.NewFakeSecretsPluginManager(t, false) migratorService := ProvideMigrateToPluginService( diff --git a/pkg/services/secrets/kvstore/test_helpers.go b/pkg/services/secrets/kvstore/test_helpers.go index 768a94d7085..7c19def969c 100644 --- a/pkg/services/secrets/kvstore/test_helpers.go +++ b/pkg/services/secrets/kvstore/test_helpers.go @@ -20,12 +20,12 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) -func NewFakeSQLSecretsKVStore(t *testing.T) *SecretsKVStoreSQL { +func NewFakeSQLSecretsKVStore(t *testing.T, sqlStore *sqlstore.SQLStore) *SecretsKVStoreSQL { t.Helper() - sqlStore := db.InitTestDB(t) secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) return NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) } diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index a0db940a2bc..322fc34d9ad 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -40,6 +40,7 @@ func NewServiceAccountsAPI( permissionService accesscontrol.ServiceAccountPermissionsService, features featuremgmt.FeatureToggles, ) *ServiceAccountsAPI { + enabled := features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) && cfg.ManagedServiceAccountsEnabled return &ServiceAccountsAPI{ cfg: cfg, service: service, @@ -48,7 +49,7 @@ func NewServiceAccountsAPI( RouterRegister: routerRegister, log: log.New("serviceaccounts.api"), permissionService: permissionService, - isExternalSAEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), + isExternalSAEnabled: enabled, } } diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service.go b/pkg/services/serviceaccounts/extsvcaccounts/service.go index d4802d993cc..bd4b7ff9cf3 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service.go @@ -28,12 +28,12 @@ import ( type ExtSvcAccountsService struct { acSvc ac.Service defaultOrgID int64 - features featuremgmt.FeatureToggles logger log.Logger metrics *metrics saSvc sa.Service skvStore kvstore.SecretsKVStore tracer tracing.Tracer + enabled bool } func ProvideExtSvcAccountsService(acSvc ac.Service, cfg *setting.Cfg, bus bus.Bus, db db.DB, features featuremgmt.FeatureToggles, reg prometheus.Registerer, saSvc *manager.ServiceAccountsService, secretsSvc secrets.Service, tracer tracing.Tracer) *ExtSvcAccountsService { @@ -43,12 +43,12 @@ func ProvideExtSvcAccountsService(acSvc ac.Service, cfg *setting.Cfg, bus bus.Bu defaultOrgID: cfg.DefaultOrgID(), logger: logger, saSvc: saSvc, - features: features, skvStore: kvstore.NewSQLSecretsKVStore(db, secretsSvc, logger), // Using SQL store to avoid a cyclic dependency tracer: tracer, + enabled: cfg.ManagedServiceAccountsEnabled && features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), } - if features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts) { + if esa.enabled { // Register the metrics esa.metrics = newMetrics(reg, esa.defaultOrgID, saSvc, logger) @@ -138,7 +138,7 @@ func (esa *ExtSvcAccountsService) GetExternalServiceNames(ctx context.Context) ( // SaveExternalService creates, updates or delete a service account (and its token) with the requested permissions. func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd *extsvcauth.ExternalServiceRegistration) (*extsvcauth.ExternalService, error) { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !esa.enabled { esa.logger.FromContext(ctx).Warn("This feature is behind a feature flag, please set it if you want to save external services") return nil, nil } @@ -184,7 +184,7 @@ func (esa *ExtSvcAccountsService) SaveExternalService(ctx context.Context, cmd * func (esa *ExtSvcAccountsService) RemoveExternalService(ctx context.Context, name string) error { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !esa.enabled { esa.logger.Warn("This feature is behind a feature flag, please set it if you want to save external services") return nil } @@ -225,7 +225,7 @@ func (esa *ExtSvcAccountsService) RemoveExtSvcAccount(ctx context.Context, orgID // ManageExtSvcAccount creates, updates or deletes the service account associated with an external service func (esa *ExtSvcAccountsService) ManageExtSvcAccount(ctx context.Context, cmd *sa.ManageExtSvcAccountCmd) (int64, error) { // This is double proofing, we should never reach here anyway the flags have already been checked. - if !esa.features.IsEnabled(ctx, featuremgmt.FlagExternalServiceAccounts) { + if !esa.enabled { esa.logger.FromContext(ctx).Warn("This feature is behind a feature flag, please set it if you want to save external services") return 0, nil } diff --git a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go index dbb1fa67c4e..c687b717ed9 100644 --- a/pkg/services/serviceaccounts/extsvcaccounts/service_test.go +++ b/pkg/services/serviceaccounts/extsvcaccounts/service_test.go @@ -41,6 +41,7 @@ func setupTestEnv(t *testing.T) *TestEnv { t.Helper() cfg := setting.NewCfg() + cfg.ManagedServiceAccountsEnabled = true fmgt := featuremgmt.WithFeatures(featuremgmt.FlagExternalServiceAccounts) env := &TestEnv{ @@ -50,13 +51,13 @@ func setupTestEnv(t *testing.T) *TestEnv { } logger := log.New("extsvcaccounts.test") env.S = &ExtSvcAccountsService{ + enabled: true, acSvc: acimpl.ProvideOSSService( cfg, env.AcStore, &resourcepermissions.FakeActionSetSvc{}, localcache.New(0, 0), fmgt, tracing.InitializeTracerForTest(), nil, nil, permreg.ProvidePermissionRegistry(), ), defaultOrgID: autoAssignOrgID, - features: fmgt, logger: logger, metrics: newMetrics(nil, autoAssignOrgID, env.SaSvc, logger), saSvc: env.SaSvc, diff --git a/pkg/services/serviceaccounts/proxy/service.go b/pkg/services/serviceaccounts/proxy/service.go index 3cb5f404721..8da7af34a41 100644 --- a/pkg/services/serviceaccounts/proxy/service.go +++ b/pkg/services/serviceaccounts/proxy/service.go @@ -38,7 +38,7 @@ func ProvideServiceAccountsProxy( s := &ServiceAccountsProxy{ log: log.New("serviceaccounts.proxy"), proxiedService: proxiedService, - isProxyEnabled: features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), + isProxyEnabled: cfg.ManagedServiceAccountsEnabled && features.IsEnabledGlobally(featuremgmt.FlagExternalServiceAccounts), } serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, accesscontrolService, routeRegister, permissionService, features) diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 4c184e56e17..338b4fcd05a 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol/testutil" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/dashboards/database" @@ -822,8 +823,11 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db), quotatest.New(false, nil)) require.NoError(t, err) - folderSvc := folderimpl.ProvideService(actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) - + folderPermissions, err := testutil.ProvideFolderPermissions(features, cfg, db) + require.NoError(t, err) + fStore := folderimpl.ProvideStore(db) + folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, + folderimpl.ProvideDashboardFolderStore(db), db, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) // create parent folder parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{ UID: "parent", diff --git a/pkg/services/sqlstore/permissions/dashboards_bench_test.go b/pkg/services/sqlstore/permissions/dashboards_bench_test.go index 4dc62cadd3a..7998e1a1adf 100644 --- a/pkg/services/sqlstore/permissions/dashboards_bench_test.go +++ b/pkg/services/sqlstore/permissions/dashboards_bench_test.go @@ -78,10 +78,13 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe quotaService := quotatest.New(false, nil) + folderPermissions := mock.NewMockedPermissionsService() dashboardWriteStore, err := database.ProvideDashboardStore(store, cfg, features, tagimpl.ProvideService(store), quotaService) require.NoError(b, err) - folderSvc := folderimpl.ProvideService(mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), store, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) + fStore := folderimpl.ProvideStore(store) + folderSvc := folderimpl.ProvideService(fStore, mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store), + store, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest()) origNewGuardian := guardian.New guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true}) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 2488ea5fddd..935254f907d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -245,6 +245,7 @@ type Cfg struct { IDResponseHeaderEnabled bool IDResponseHeaderPrefix string IDResponseHeaderNamespaces map[string]struct{} + ManagedServiceAccountsEnabled bool // AWS Plugin Auth AWSAllowedAuthProviders []string @@ -262,6 +263,7 @@ type Cfg struct { // OAuth OAuthAutoLogin bool + OAuthLoginErrorMessage string OAuthCookieMaxAge int OAuthAllowInsecureEmailLookup bool OAuthRefreshTokenServerLockMinWaitMs int64 @@ -1620,6 +1622,8 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { cfg.Logger.Warn("[Deprecated] The oauth_auto_login configuration setting is deprecated. Please use auto_login inside auth provider section instead.") } + // Default to the translation key used in the frontend + cfg.OAuthLoginErrorMessage = valueAsString(auth, "oauth_login_error_message", "oauth.login.error") cfg.OAuthCookieMaxAge = auth.Key("oauth_state_cookie_max_age").MustInt(600) cfg.OAuthRefreshTokenServerLockMinWaitMs = auth.Key("oauth_refresh_token_server_lock_min_wait_ms").MustInt64(1000) cfg.SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "") @@ -1668,6 +1672,10 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { for _, provider := range util.SplitString(providers) { cfg.SSOSettingsConfigurableProviders[provider] = true } + + // Managed Service Accounts + cfg.ManagedServiceAccountsEnabled = auth.Key("managed_service_accounts_enabled").MustBool(false) + return nil } diff --git a/pkg/storage/unified/resource/noop.go b/pkg/storage/unified/resource/noop.go index 9553c607990..5a1a6a22820 100644 --- a/pkg/storage/unified/resource/noop.go +++ b/pkg/storage/unified/resource/noop.go @@ -35,6 +35,10 @@ func (n *noopService) Read(context.Context, *ReadRequest) (*ReadResponse, error) return nil, ErrNotImplementedYet } +func (n *noopService) Search(context.Context, *SearchRequest) (*SearchResponse, error) { + return nil, ErrNotImplementedYet +} + func (n *noopService) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { return nil, ErrNotImplementedYet } diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index 7fa31ba23b1..44e5be68f8c 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -173,7 +173,7 @@ func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber { // Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead. func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{26, 0} + return file_resource_proto_rawDescGZIP(), []int{28, 0} } type ResourceKey struct { @@ -1614,6 +1614,100 @@ func (x *WatchEvent) GetPrevious() *WatchEvent_Resource { return nil } +type SearchRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` +} + +func (x *SearchRequest) Reset() { + *x = SearchRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchRequest) ProtoMessage() {} + +func (x *SearchRequest) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead. +func (*SearchRequest) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{20} +} + +func (x *SearchRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +type SearchResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Items []*ResourceWrapper `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` +} + +func (x *SearchResponse) Reset() { + *x = SearchResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_resource_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SearchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchResponse) ProtoMessage() {} + +func (x *SearchResponse) ProtoReflect() protoreflect.Message { + mi := &file_resource_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead. +func (*SearchResponse) Descriptor() ([]byte, []int) { + return file_resource_proto_rawDescGZIP(), []int{21} +} + +func (x *SearchResponse) GetItems() []*ResourceWrapper { + if x != nil { + return x.Items + } + return nil +} + type HistoryRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1632,7 +1726,7 @@ type HistoryRequest struct { func (x *HistoryRequest) Reset() { *x = HistoryRequest{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[20] + mi := &file_resource_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1645,7 +1739,7 @@ func (x *HistoryRequest) String() string { func (*HistoryRequest) ProtoMessage() {} func (x *HistoryRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[20] + mi := &file_resource_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1658,7 +1752,7 @@ func (x *HistoryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HistoryRequest.ProtoReflect.Descriptor instead. func (*HistoryRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{20} + return file_resource_proto_rawDescGZIP(), []int{22} } func (x *HistoryRequest) GetNextPageToken() string { @@ -1706,7 +1800,7 @@ type HistoryResponse struct { func (x *HistoryResponse) Reset() { *x = HistoryResponse{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[21] + mi := &file_resource_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1719,7 +1813,7 @@ func (x *HistoryResponse) String() string { func (*HistoryResponse) ProtoMessage() {} func (x *HistoryResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[21] + mi := &file_resource_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1732,7 +1826,7 @@ func (x *HistoryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HistoryResponse.ProtoReflect.Descriptor instead. func (*HistoryResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{21} + return file_resource_proto_rawDescGZIP(), []int{23} } func (x *HistoryResponse) GetItems() []*ResourceMeta { @@ -1781,7 +1875,7 @@ type OriginRequest struct { func (x *OriginRequest) Reset() { *x = OriginRequest{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[22] + mi := &file_resource_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1794,7 +1888,7 @@ func (x *OriginRequest) String() string { func (*OriginRequest) ProtoMessage() {} func (x *OriginRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[22] + mi := &file_resource_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1807,7 +1901,7 @@ func (x *OriginRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use OriginRequest.ProtoReflect.Descriptor instead. func (*OriginRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{22} + return file_resource_proto_rawDescGZIP(), []int{24} } func (x *OriginRequest) GetNextPageToken() string { @@ -1862,7 +1956,7 @@ type ResourceOriginInfo struct { func (x *ResourceOriginInfo) Reset() { *x = ResourceOriginInfo{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[23] + mi := &file_resource_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1875,7 +1969,7 @@ func (x *ResourceOriginInfo) String() string { func (*ResourceOriginInfo) ProtoMessage() {} func (x *ResourceOriginInfo) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[23] + mi := &file_resource_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1888,7 +1982,7 @@ func (x *ResourceOriginInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceOriginInfo.ProtoReflect.Descriptor instead. func (*ResourceOriginInfo) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{23} + return file_resource_proto_rawDescGZIP(), []int{25} } func (x *ResourceOriginInfo) GetKey() *ResourceKey { @@ -1957,7 +2051,7 @@ type OriginResponse struct { func (x *OriginResponse) Reset() { *x = OriginResponse{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[24] + mi := &file_resource_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1970,7 +2064,7 @@ func (x *OriginResponse) String() string { func (*OriginResponse) ProtoMessage() {} func (x *OriginResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[24] + mi := &file_resource_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1983,7 +2077,7 @@ func (x *OriginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use OriginResponse.ProtoReflect.Descriptor instead. func (*OriginResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{24} + return file_resource_proto_rawDescGZIP(), []int{26} } func (x *OriginResponse) GetItems() []*ResourceOriginInfo { @@ -2025,7 +2119,7 @@ type HealthCheckRequest struct { func (x *HealthCheckRequest) Reset() { *x = HealthCheckRequest{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[25] + mi := &file_resource_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2038,7 +2132,7 @@ func (x *HealthCheckRequest) String() string { func (*HealthCheckRequest) ProtoMessage() {} func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[25] + mi := &file_resource_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2051,7 +2145,7 @@ func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead. func (*HealthCheckRequest) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{25} + return file_resource_proto_rawDescGZIP(), []int{27} } func (x *HealthCheckRequest) GetService() string { @@ -2072,7 +2166,7 @@ type HealthCheckResponse struct { func (x *HealthCheckResponse) Reset() { *x = HealthCheckResponse{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[26] + mi := &file_resource_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2085,7 +2179,7 @@ func (x *HealthCheckResponse) String() string { func (*HealthCheckResponse) ProtoMessage() {} func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[26] + mi := &file_resource_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2098,7 +2192,7 @@ func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead. func (*HealthCheckResponse) Descriptor() ([]byte, []int) { - return file_resource_proto_rawDescGZIP(), []int{26} + return file_resource_proto_rawDescGZIP(), []int{28} } func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus { @@ -2120,7 +2214,7 @@ type WatchEvent_Resource struct { func (x *WatchEvent_Resource) Reset() { *x = WatchEvent_Resource{} if protoimpl.UnsafeEnabled { - mi := &file_resource_proto_msgTypes[27] + mi := &file_resource_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2133,7 +2227,7 @@ func (x *WatchEvent_Resource) String() string { func (*WatchEvent_Resource) ProtoMessage() {} func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message { - mi := &file_resource_proto_msgTypes[27] + mi := &file_resource_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2347,124 +2441,135 @@ var file_resource_proto_rawDesc = []byte{ 0x09, 0x0a, 0x05, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, - 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x9a, - 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, - 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, - 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, + 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x25, + 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x41, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65, + 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x0e, 0x48, 0x69, 0x73, + 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, + 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0xbf, 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, + 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x8e, 0x01, 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, + 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, + 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x77, - 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, - 0x73, 0x68, 0x6f, 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0xbf, 0x01, 0x0a, 0x0f, - 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x2c, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, - 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x8e, 0x01, - 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, - 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, - 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5, - 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, - 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x23, - 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, - 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, - 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0xc4, 0x01, 0x0a, 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, - 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, - 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, - 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x2e, 0x0a, - 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, - 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, - 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, - 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, - 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, 0x14, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, - 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, - 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, - 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, - 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, + 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, + 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, + 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, + 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, + 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x22, 0xc4, 0x01, 0x0a, 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, + 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, + 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, + 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, + 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, + 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, + 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, + 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, - 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, - 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, - 0x32, 0x8c, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, - 0x65, 0x78, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, + 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, + 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, + 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, + 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xc9, 0x01, 0x0a, 0x0d, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x3b, 0x0a, 0x06, + 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, + 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, + 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, + 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, - 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, - 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, - 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, - 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, + 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, + 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, + 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -2480,7 +2585,7 @@ func file_resource_proto_rawDescGZIP() []byte { } var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 30) var file_resource_proto_goTypes = []any{ (ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch (WatchEvent_Type)(0), // 1: resource.WatchEvent.Type @@ -2505,14 +2610,16 @@ var file_resource_proto_goTypes = []any{ (*ListResponse)(nil), // 20: resource.ListResponse (*WatchRequest)(nil), // 21: resource.WatchRequest (*WatchEvent)(nil), // 22: resource.WatchEvent - (*HistoryRequest)(nil), // 23: resource.HistoryRequest - (*HistoryResponse)(nil), // 24: resource.HistoryResponse - (*OriginRequest)(nil), // 25: resource.OriginRequest - (*ResourceOriginInfo)(nil), // 26: resource.ResourceOriginInfo - (*OriginResponse)(nil), // 27: resource.OriginResponse - (*HealthCheckRequest)(nil), // 28: resource.HealthCheckRequest - (*HealthCheckResponse)(nil), // 29: resource.HealthCheckResponse - (*WatchEvent_Resource)(nil), // 30: resource.WatchEvent.Resource + (*SearchRequest)(nil), // 23: resource.SearchRequest + (*SearchResponse)(nil), // 24: resource.SearchResponse + (*HistoryRequest)(nil), // 25: resource.HistoryRequest + (*HistoryResponse)(nil), // 26: resource.HistoryResponse + (*OriginRequest)(nil), // 27: resource.OriginRequest + (*ResourceOriginInfo)(nil), // 28: resource.ResourceOriginInfo + (*OriginResponse)(nil), // 29: resource.OriginResponse + (*HealthCheckRequest)(nil), // 30: resource.HealthCheckRequest + (*HealthCheckResponse)(nil), // 31: resource.HealthCheckResponse + (*WatchEvent_Resource)(nil), // 32: resource.WatchEvent.Resource } var file_resource_proto_depIdxs = []int32{ 7, // 0: resource.ErrorResult.details:type_name -> resource.ErrorDetails @@ -2534,39 +2641,42 @@ var file_resource_proto_depIdxs = []int32{ 6, // 16: resource.ListResponse.error:type_name -> resource.ErrorResult 18, // 17: resource.WatchRequest.options:type_name -> resource.ListOptions 1, // 18: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type - 30, // 19: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource - 30, // 20: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource - 3, // 21: resource.HistoryRequest.key:type_name -> resource.ResourceKey - 5, // 22: resource.HistoryResponse.items:type_name -> resource.ResourceMeta - 6, // 23: resource.HistoryResponse.error:type_name -> resource.ErrorResult - 3, // 24: resource.OriginRequest.key:type_name -> resource.ResourceKey - 3, // 25: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey - 26, // 26: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo - 6, // 27: resource.OriginResponse.error:type_name -> resource.ErrorResult - 2, // 28: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus - 15, // 29: resource.ResourceStore.Read:input_type -> resource.ReadRequest - 9, // 30: resource.ResourceStore.Create:input_type -> resource.CreateRequest - 11, // 31: resource.ResourceStore.Update:input_type -> resource.UpdateRequest - 13, // 32: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest - 19, // 33: resource.ResourceStore.List:input_type -> resource.ListRequest - 21, // 34: resource.ResourceStore.Watch:input_type -> resource.WatchRequest - 23, // 35: resource.ResourceIndex.History:input_type -> resource.HistoryRequest - 25, // 36: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest - 28, // 37: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest - 16, // 38: resource.ResourceStore.Read:output_type -> resource.ReadResponse - 10, // 39: resource.ResourceStore.Create:output_type -> resource.CreateResponse - 12, // 40: resource.ResourceStore.Update:output_type -> resource.UpdateResponse - 14, // 41: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse - 20, // 42: resource.ResourceStore.List:output_type -> resource.ListResponse - 22, // 43: resource.ResourceStore.Watch:output_type -> resource.WatchEvent - 24, // 44: resource.ResourceIndex.History:output_type -> resource.HistoryResponse - 27, // 45: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse - 29, // 46: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse - 38, // [38:47] is the sub-list for method output_type - 29, // [29:38] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 32, // 19: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource + 32, // 20: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource + 4, // 21: resource.SearchResponse.items:type_name -> resource.ResourceWrapper + 3, // 22: resource.HistoryRequest.key:type_name -> resource.ResourceKey + 5, // 23: resource.HistoryResponse.items:type_name -> resource.ResourceMeta + 6, // 24: resource.HistoryResponse.error:type_name -> resource.ErrorResult + 3, // 25: resource.OriginRequest.key:type_name -> resource.ResourceKey + 3, // 26: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey + 28, // 27: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo + 6, // 28: resource.OriginResponse.error:type_name -> resource.ErrorResult + 2, // 29: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus + 15, // 30: resource.ResourceStore.Read:input_type -> resource.ReadRequest + 9, // 31: resource.ResourceStore.Create:input_type -> resource.CreateRequest + 11, // 32: resource.ResourceStore.Update:input_type -> resource.UpdateRequest + 13, // 33: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest + 19, // 34: resource.ResourceStore.List:input_type -> resource.ListRequest + 21, // 35: resource.ResourceStore.Watch:input_type -> resource.WatchRequest + 23, // 36: resource.ResourceIndex.Search:input_type -> resource.SearchRequest + 25, // 37: resource.ResourceIndex.History:input_type -> resource.HistoryRequest + 27, // 38: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest + 30, // 39: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest + 16, // 40: resource.ResourceStore.Read:output_type -> resource.ReadResponse + 10, // 41: resource.ResourceStore.Create:output_type -> resource.CreateResponse + 12, // 42: resource.ResourceStore.Update:output_type -> resource.UpdateResponse + 14, // 43: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse + 20, // 44: resource.ResourceStore.List:output_type -> resource.ListResponse + 22, // 45: resource.ResourceStore.Watch:output_type -> resource.WatchEvent + 24, // 46: resource.ResourceIndex.Search:output_type -> resource.SearchResponse + 26, // 47: resource.ResourceIndex.History:output_type -> resource.HistoryResponse + 29, // 48: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse + 31, // 49: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse + 40, // [40:50] is the sub-list for method output_type + 30, // [30:40] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name } func init() { file_resource_proto_init() } @@ -2816,7 +2926,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[20].Exporter = func(v any, i int) any { - switch v := v.(*HistoryRequest); i { + switch v := v.(*SearchRequest); i { case 0: return &v.state case 1: @@ -2828,7 +2938,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[21].Exporter = func(v any, i int) any { - switch v := v.(*HistoryResponse); i { + switch v := v.(*SearchResponse); i { case 0: return &v.state case 1: @@ -2840,7 +2950,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[22].Exporter = func(v any, i int) any { - switch v := v.(*OriginRequest); i { + switch v := v.(*HistoryRequest); i { case 0: return &v.state case 1: @@ -2852,7 +2962,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[23].Exporter = func(v any, i int) any { - switch v := v.(*ResourceOriginInfo); i { + switch v := v.(*HistoryResponse); i { case 0: return &v.state case 1: @@ -2864,7 +2974,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[24].Exporter = func(v any, i int) any { - switch v := v.(*OriginResponse); i { + switch v := v.(*OriginRequest); i { case 0: return &v.state case 1: @@ -2876,7 +2986,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[25].Exporter = func(v any, i int) any { - switch v := v.(*HealthCheckRequest); i { + switch v := v.(*ResourceOriginInfo); i { case 0: return &v.state case 1: @@ -2888,7 +2998,7 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[26].Exporter = func(v any, i int) any { - switch v := v.(*HealthCheckResponse); i { + switch v := v.(*OriginResponse); i { case 0: return &v.state case 1: @@ -2900,6 +3010,30 @@ func file_resource_proto_init() { } } file_resource_proto_msgTypes[27].Exporter = func(v any, i int) any { + switch v := v.(*HealthCheckRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[28].Exporter = func(v any, i int) any { + switch v := v.(*HealthCheckResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_resource_proto_msgTypes[29].Exporter = func(v any, i int) any { switch v := v.(*WatchEvent_Resource); i { case 0: return &v.state @@ -2918,7 +3052,7 @@ func file_resource_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_resource_proto_rawDesc, NumEnums: 3, - NumMessages: 28, + NumMessages: 30, NumExtensions: 0, NumServices: 3, }, diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index e298ff77aa9..9859ea51994 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -137,8 +137,8 @@ message CreateRequest { // If name is not set, a unique name will be generated // The resourceVersion should not be set ResourceKey key = 1; - - // The resource JSON. + + // The resource JSON. bytes value = 2; } @@ -157,7 +157,7 @@ message UpdateRequest { // The current resource version int64 resource_version = 2; - // The resource JSON. + // The resource JSON. bytes value = 3; } @@ -245,7 +245,7 @@ message ListRequest { // The resource version int64 resource_version = 2; - + // List options ResourceVersionMatch version_match = 3; @@ -302,8 +302,8 @@ message WatchEvent { ADDED = 1; MODIFIED = 2; DELETED = 3; - BOOKMARK = 4; - ERROR = 5; + BOOKMARK = 4; + ERROR = 5; } message Resource { @@ -324,6 +324,14 @@ message WatchEvent { Resource previous = 4; } +message SearchRequest { + string query = 1; +} + +message SearchResponse { + repeated ResourceWrapper items = 1; +} + message HistoryRequest { // Starting from the requested page (other query parameters must match!) string next_page_token = 1; @@ -418,7 +426,7 @@ message HealthCheckResponse { // This provides the CRUD+List+Watch support needed for a k8s apiserver // The semantics and behaviors of this service are constrained by kubernetes -// This does not understand the resource schemas, only deals with json bytes +// This does not understand the resource schemas, only deals with json bytes // Clients should not use this interface directly; it is for use in API Servers service ResourceStore { rpc Read(ReadRequest) returns (ReadResponse); @@ -427,12 +435,12 @@ service ResourceStore { rpc Delete(DeleteRequest) returns (DeleteResponse); // The results *may* include values that should not be returned to the user - // This will perform best-effort filtering to increase performace. + // This will perform best-effort filtering to increase performace. // NOTE: storage.Interface is ultimatly responsible for the final filtering rpc List(ListRequest) returns (ListResponse); // The results *may* include values that should not be returned to the user - // This will perform best-effort filtering to increase performace. + // This will perform best-effort filtering to increase performace. // NOTE: storage.Interface is ultimatly responsible for the final filtering rpc Watch(WatchRequest) returns (stream WatchEvent); } @@ -440,7 +448,7 @@ service ResourceStore { // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics service ResourceIndex { - // TODO: rpc Search(...) ... eventually a typed response + rpc Search(SearchRequest) returns (SearchResponse); // Show resource history (and trash) rpc History(HistoryRequest) returns (HistoryResponse); diff --git a/pkg/storage/unified/resource/resource_grpc.pb.go b/pkg/storage/unified/resource/resource_grpc.pb.go index 9e1db6eb635..f349db5c566 100644 --- a/pkg/storage/unified/resource/resource_grpc.pb.go +++ b/pkg/storage/unified/resource/resource_grpc.pb.go @@ -348,6 +348,7 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{ } const ( + ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search" ResourceIndex_History_FullMethodName = "/resource.ResourceIndex/History" ResourceIndex_Origin_FullMethodName = "/resource.ResourceIndex/Origin" ) @@ -359,6 +360,7 @@ const ( // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics type ResourceIndexClient interface { + Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) // Show resource history (and trash) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) // Used for efficient provisioning @@ -373,6 +375,16 @@ func NewResourceIndexClient(cc grpc.ClientConnInterface) ResourceIndexClient { return &resourceIndexClient{cc} } +func (c *resourceIndexClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SearchResponse) + err := c.cc.Invoke(ctx, ResourceIndex_Search_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *resourceIndexClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(HistoryResponse) @@ -400,6 +412,7 @@ func (c *resourceIndexClient) Origin(ctx context.Context, in *OriginRequest, opt // Unlike the ResourceStore, this service can be exposed to clients directly // It should be implemented with efficient indexes and does not need read-after-write semantics type ResourceIndexServer interface { + Search(context.Context, *SearchRequest) (*SearchResponse, error) // Show resource history (and trash) History(context.Context, *HistoryRequest) (*HistoryResponse, error) // Used for efficient provisioning @@ -410,6 +423,9 @@ type ResourceIndexServer interface { type UnimplementedResourceIndexServer struct { } +func (UnimplementedResourceIndexServer) Search(context.Context, *SearchRequest) (*SearchResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Search not implemented") +} func (UnimplementedResourceIndexServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method History not implemented") } @@ -428,6 +444,24 @@ func RegisterResourceIndexServer(s grpc.ServiceRegistrar, srv ResourceIndexServe s.RegisterService(&ResourceIndex_ServiceDesc, srv) } +func _ResourceIndex_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SearchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ResourceIndexServer).Search(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ResourceIndex_Search_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ResourceIndexServer).Search(ctx, req.(*SearchRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _ResourceIndex_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(HistoryRequest) if err := dec(in); err != nil { @@ -471,6 +505,10 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{ ServiceName: "resource.ResourceIndex", HandlerType: (*ResourceIndexServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "Search", + Handler: _ResourceIndex_Search_Handler, + }, { MethodName: "History", Handler: _ResourceIndex_History_Handler, diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index a31ca6fe37b..8484653049a 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -99,6 +99,25 @@ type ResourceServerOptions struct { Now func() int64 } +type indexServer struct{} + +func (s indexServer) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) { + res := &SearchResponse{} + return res, nil +} + +func (s indexServer) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) { + return nil, nil +} + +func (s indexServer) Origin(ctx context.Context, req *OriginRequest) (*OriginResponse, error) { + return nil, nil +} + +func NewResourceIndexServer() ResourceIndexServer { + return indexServer{} +} + func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { if opts.Tracer == nil { opts.Tracer = noop.NewTracerProvider().Tracer("resource-server") @@ -107,9 +126,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { if opts.Backend == nil { return nil, fmt.Errorf("missing Backend implementation") } - if opts.Index == nil { - opts.Index = &noopService{} - } + if opts.Diagnostics == nil { opts.Diagnostics = &noopService{} } @@ -603,6 +620,13 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error { } } +func (s *server) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) { + if err := s.Init(ctx); err != nil { + return nil, err + } + return s.index.Search(ctx, req) +} + // History implements ResourceServer. func (s *server) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) { if err := s.Init(ctx); err != nil { diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go index a5caf56ec37..58770875bc3 100644 --- a/pkg/storage/unified/sql/server.go +++ b/pkg/storage/unified/sql/server.go @@ -27,5 +27,9 @@ func NewResourceServer(db infraDB.DB, cfg *setting.Cfg, features featuremgmt.Fea opts.Diagnostics = store opts.Lifecycle = store + if features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) { + opts.Index = resource.NewResourceIndexServer() + } + return resource.NewResourceServer(opts) } diff --git a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go index 3e65c662567..c90bb4d9382 100644 --- a/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go +++ b/pkg/tests/apis/alerting/notifications/receivers/receiver_test.go @@ -396,9 +396,10 @@ func TestIntegrationResourcePermissions(t *testing.T) { assert.Equalf(t, expectedGetWithMetadata, got, "Expected %v but got %v", expectedGetWithMetadata, got) }) } else { - t.Run("should be forbidden to list receivers", func(t *testing.T) { - _, err := client.List(ctx, v1.ListOptions{}) - require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + t.Run("list receivers should be empty", func(t *testing.T) { + list, err := client.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + require.Emptyf(t, list.Items, "Expected no receivers but got %v", list.Items) }) t.Run("should be forbidden to read receiver by name", func(t *testing.T) { @@ -640,9 +641,10 @@ func TestIntegrationAccessControl(t *testing.T) { }) }) } else { - t.Run("should be forbidden to list receivers", func(t *testing.T) { - _, err := client.List(ctx, v1.ListOptions{}) - require.Truef(t, errors.IsForbidden(err), "should get Forbidden error but got %s", err) + t.Run("list receivers should be empty", func(t *testing.T) { + list, err := client.List(ctx, v1.ListOptions{}) + require.NoError(t, err) + require.Emptyf(t, list.Items, "Expected no receivers but got %v", list.Items) }) t.Run("should be forbidden to read receiver by name", func(t *testing.T) { diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index edfd10d6ed7..aea2069d750 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -40,8 +40,10 @@ func TestIntegrationFoldersApp(t *testing.T) { helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ AppModeProduction: true, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). }) t.Run("Check discovery client", func(t *testing.T) { @@ -108,8 +110,10 @@ func TestIntegrationFoldersApp(t *testing.T) { AppModeProduction: true, DisableAnonymous: true, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -124,8 +128,10 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -140,8 +146,10 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -156,8 +164,10 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -172,8 +182,10 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -188,8 +200,10 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -204,8 +218,10 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). })) }) @@ -223,8 +239,10 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). }) // Clear the collection before starting (etcd) @@ -252,8 +270,10 @@ func TestIntegrationFoldersApp(t *testing.T) { }, }, EnableFeatureToggles: []string{ - featuremgmt.FlagKubernetesFolders, + featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs, }, + // Not including featuremgmt.FlagKubernetesFolders because we refer to the k8s client directly in doFolderTests(). + // This allows us to access the legacy api (which gets bypassed by featuremgmt.FlagKubernetesFolders). }) // Clear the collection before starting (etcd) diff --git a/pkg/tsdb/elasticsearch/elasticsearch.go b/pkg/tsdb/elasticsearch/elasticsearch.go index 374ccb5d2da..2b7a43725ba 100644 --- a/pkg/tsdb/elasticsearch/elasticsearch.go +++ b/pkg/tsdb/elasticsearch/elasticsearch.go @@ -102,11 +102,11 @@ func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.Ins timeField, ok := jsonData["timeField"].(string) if !ok { - return nil, errors.New("timeField cannot be cast to string") + return nil, exp.DownstreamError(errors.New("timeField cannot be cast to string"), false) } if timeField == "" { - return nil, errors.New("elasticsearch time field name is required") + return nil, exp.DownstreamError(errors.New("elasticsearch time field name is required"), false) } logLevelField, ok := jsonData["logLevelField"].(string) diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index ac04c23d278..e2d491516ff 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -5416,6 +5416,9 @@ "message": { "type": "string" }, + "name": { + "type": "string" + }, "refId": { "type": "string" }, diff --git a/public/api-merged.json b/public/api-merged.json index 87416169e3a..a149ee3ead8 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2600,6 +2600,12 @@ "name": "uid", "in": "path", "required": true + }, + { + "type": "string", + "description": "Sort with value latest to return results sorted in descending order.", + "name": "sort", + "in": "query" } ], "responses": { @@ -12388,6 +12394,14 @@ } } }, + "AlertRuleEditorSettings": { + "type": "object", + "properties": { + "simplified_query_and_expressions_section": { + "type": "boolean" + } + } + }, "AlertRuleExport": { "type": "object", "title": "AlertRuleExport is the provisioned file export of models.AlertRule.", @@ -12511,6 +12525,14 @@ } } }, + "AlertRuleMetadata": { + "type": "object", + "properties": { + "editor_settings": { + "$ref": "#/definitions/AlertRuleEditorSettings" + } + } + }, "AlertRuleNotificationSettings": { "type": "object", "required": [ @@ -15793,6 +15815,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "namespace_uid": { "type": "string" }, @@ -15880,12 +15905,25 @@ "GettableRuleGroupConfig": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { @@ -18081,6 +18119,9 @@ "is_paused": { "type": "boolean" }, + "metadata": { + "$ref": "#/definitions/AlertRuleMetadata" + }, "no_data_state": { "type": "string", "enum": [ @@ -18119,17 +18160,36 @@ "PostableRuleGroupConfig": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { "$ref": "#/definitions/PostableExtendedRuleNode" } + }, + "source_tenants": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -19594,12 +19654,25 @@ "RuleGroupConfigResponse": { "type": "object", "properties": { + "align_evaluation_time_on_interval": { + "type": "boolean" + }, + "evaluation_delay": { + "type": "string" + }, "interval": { "$ref": "#/definitions/Duration" }, + "limit": { + "type": "integer", + "format": "int64" + }, "name": { "type": "string" }, + "query_offset": { + "type": "string" + }, "rules": { "type": "array", "items": { diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index 768a0ff0dff..91a9e5efd4b 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -1,8 +1,8 @@ import { Action, KBarProvider } from 'kbar'; import { Component, ComponentType } from 'react'; import { Provider } from 'react-redux'; -import { Redirect, Switch, RouteComponentProps } from 'react-router-dom'; -import { CompatRoute } from 'react-router-dom-v5-compat'; +import { Switch, RouteComponentProps } from 'react-router-dom'; +import { CompatRoute, Navigate } from 'react-router-dom-v5-compat'; import { config, navigationLogger, reportInteraction } from '@grafana/runtime'; import { ErrorBoundaryAlert, GlobalStyles, PortalContainer } from '@grafana/ui'; @@ -67,7 +67,7 @@ export class AppWrapper extends Component { // TODO[Router]: test this logic if (roles?.length) { if (!roles.some((r: string) => contextSrv.hasRole(r))) { - return ; + return ; } } diff --git a/public/app/app.ts b/public/app/app.ts index 97e50653ca5..df42edc1df0 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -409,7 +409,13 @@ function handleRedirectTo(): void { } window.sessionStorage.removeItem(RedirectToUrlKey); - locationService.replace(decodeURIComponent(redirectTo)); + const decodedRedirectTo = decodeURIComponent(redirectTo); + if (decodedRedirectTo.startsWith('/goto/')) { + // In this case there should be a request to the backend + window.location.replace(decodedRedirectTo); + } else { + locationService.replace(decodedRedirectTo); + } } export default new GrafanaApp(); diff --git a/public/app/core/components/AccessControl/types.ts b/public/app/core/components/AccessControl/types.ts index 80ceca019bc..f4e4363de5f 100644 --- a/public/app/core/components/AccessControl/types.ts +++ b/public/app/core/components/AccessControl/types.ts @@ -1,6 +1,8 @@ +import { AccessControlAction } from 'app/types'; + export type ResourcePermission = { id: number; - resourceId: string; + resourceId?: string; isManaged: boolean; isInherited: boolean; isServiceAccount: boolean; @@ -11,8 +13,9 @@ export type ResourcePermission = { teamId?: number; teamAvatarUrl?: string; builtInRole?: string; - actions: string[]; + actions: AccessControlAction[]; permission: string; + roleName?: string; warning?: string; }; diff --git a/public/app/core/components/AppChrome/AppChromeMenu.tsx b/public/app/core/components/AppChrome/AppChromeMenu.tsx index 7926e41aae9..cf02b6a7c48 100644 --- a/public/app/core/components/AppChrome/AppChromeMenu.tsx +++ b/public/app/core/components/AppChrome/AppChromeMenu.tsx @@ -91,7 +91,7 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { left: 0, position: 'fixed', right: 0, - top: searchBarHidden ? 0 : TOP_BAR_LEVEL_HEIGHT, + top: searchBarHidden || config.featureToggles.singleTopNav ? 0 : TOP_BAR_LEVEL_HEIGHT, zIndex: theme.zIndex.modalBackdrop, [theme.breakpoints.up('md')]: { @@ -107,7 +107,7 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { // Needs to below navbar should we change the navbarFixed? add add a new level? zIndex: theme.zIndex.modal, position: 'fixed', - top: searchBarHidden ? 0 : TOP_BAR_LEVEL_HEIGHT, + top: searchBarHidden || config.featureToggles.singleTopNav ? 0 : TOP_BAR_LEVEL_HEIGHT, backgroundColor: theme.colors.background.primary, flex: '1 1 0', diff --git a/public/app/core/components/AppChrome/AppChromeUpdate.tsx b/public/app/core/components/AppChrome/AppChromeUpdate.tsx index d7f03d2da4d..ef92b81903a 100644 --- a/public/app/core/components/AppChrome/AppChromeUpdate.tsx +++ b/public/app/core/components/AppChrome/AppChromeUpdate.tsx @@ -7,8 +7,7 @@ export interface AppChromeUpdateProps { actions?: React.ReactNode; } /** - * This needs to be moved to @grafana/ui or runtime. - * This is the way core pages and plugins update the breadcrumbs and page toolbar actions + * @deprecated This component is deprecated and will be removed in a future release. */ export const AppChromeUpdate = React.memo(({ actions }: AppChromeUpdateProps) => { const { chrome } = useGrafana(); diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index d7deaaeb3cd..b2be2f189b1 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -13,6 +13,7 @@ import { setBookmark } from 'app/core/reducers/navBarTree'; import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index'; import { useDispatch, useSelector } from 'app/types'; +import { MegaMenuHeader } from './MegaMenuHeader'; import { MegaMenuItem } from './MegaMenuItem'; import { usePinnedItems } from './hooks'; import { enrichWithInteractionTracking, findByUrl, getActiveItem } from './utils'; @@ -62,6 +63,10 @@ export const MegaMenu = memo( const activeItem = getActiveItem(navItems, state.sectionNav.node, location.pathname); + const handleMegaMenu = () => { + chrome.setMegaMenuOpen(!state.megaMenuOpen); + }; + const handleDockedMenu = () => { chrome.setMegaMenuDocked(!state.megaMenuDocked); if (state.megaMenuDocked) { @@ -109,22 +114,26 @@ export const MegaMenu = memo( return (
      -
      - - -
      + {config.featureToggles.singleTopNav ? ( + + ) : ( +
      + + +
      + )}